Luyện Phỏng Vấn IT — 2000+ Câu Hỏi Phỏng Vấn IT Có Đáp Án 2026

Danh mục

Linux & OS iconLinux & OS

Process là đơn vị thực thi độc lập với không gian bộ nhớ riêng (virtual address space, heap, stack, file descriptors). Thread là đơn vị thực thi nhẹ hơn bên trong process: các thread cùng process chia sẻ heap/code/file descriptors nhưng có stack riêng. Tạo process tốn kém hơn thread (fork copy page tables), giao tiếp giữa process phức tạp hơn (IPC), nhưng cô lập tốt hơn — crash một process không ảnh hưởng process khác.

Multi-process phù hợp: Chrome (mỗi tab là process riêng để cô lập crash/security), Node.js cluster module để tận dụng CPU cores. Multi-thread phù hợp: ứng dụng cần chia sẻ dữ liệu nhiều và frequent (Java web server, game engine). Với Python, GIL (Global Interpreter Lock) ngăn true parallelism trong threads — phải dùng multiprocessing. Go goroutines là green threads cực nhẹ (~2KB stack), runtime scheduler map M goroutines lên N OS threads.

Context switching là quá trình OS lưu trạng thái CPU (registers, program counter, stack pointer) của process/thread hiện tại vào PCB (Process Control Block), rồi nạp trạng thái của process/thread kế tiếp để chạy.

Chi phí bao gồm:

  1. Thời gian lưu/nạp CPU state (~microseconds)
  2. Cache invalidation — khi switch process, CPU cache (L1/L2) bị flush, process mới phải warm up cache lại từ đầu (cold start)
  3. TLB flush khi switch process (vì địa chỉ ảo khác nhau). Thread switching trong cùng process nhẹ hơn vì không flush TLB. Ảnh hưởng thực tế: hệ thống với quá nhiều thread (C10K problem) tốn nhiều thời gian context switching hơn là làm việc thực. Đó là lý do Node.js single-threaded event loop và Go goroutines thắng trong high-concurrency — ít context switch hơn 1000 OS threads. Lập trình viên nên tránh: vòng lặp tight với sleep(1ms), quá nhiều thread chờ I/O — dùng async/await thay thế

Các thuật toán scheduling khác nhau về cách ưu tiên process: FCFS đơn giản nhưng tệ với latency, Round Robin fair nhất, SJF tối ưu throughput nhưng gây starvation.

  • FCFS (First-Come First-Served): process đến trước chạy trước — đơn giản nhưng convoy effect: một process dài block tất cả process ngắn phía sau, average waiting time cao.
  • Round Robin (RR): mỗi process chạy một time quantum (10-100ms) rồi preempt — fair, responsive, phù hợp time-sharing systems; quantum quá nhỏ tốn context switch, quá lớn thành FCFS.
  • SJF (Shortest Job First): chạy process có burst time ngắn nhất trước — tối ưu average waiting time nhưng starvation cho process dài, và không biết trước burst time trong thực tế.
  • Priority Scheduling: mỗi process có độ ưu tiên, process ưu tiên cao chạy trước — có thể bị starvation (giải quyết bằng aging: tăng priority theo thời gian chờ).
  • Multilevel Feedback Queue (Linux CFS): kết hợp nhiều queue, tự động điều chỉnh — đây là algorithm Linux kernel dùng, fair nhưng ưu tiên interactive process hơn batch.

Concurrency là khả năng xử lý nhiều task đồng thời về mặt logic — các task có thể interleave nhau (chạy một ít rồi nhường, rồi lại chạy) nhưng không nhất thiết cùng lúc trên nhiều CPU. Parallelism là thực sự chạy nhiều task cùng một lúc trên nhiều CPU core. Rob Pike (Go): 'Concurrency là về cấu trúc, Parallelism là về thực thi.'

Ví dụ: Node.js event loop là concurrent nhưng không parallel (single-threaded) — handle 10,000 I/O operations concurrently nhờ non-blocking I/O, nhưng chỉ 1 lúc chạy 1 JavaScript callback. Go runtime chạy goroutines concurrently VÀ parallel: GOMAXPROCS goroutines có thể thực sự chạy song song trên nhiều OS thread. Thực tế lập trình: concurrency giải quyết throughput (nhiều I/O operations); parallelism giải quyết CPU-bound tasks (image processing, ML inference). Async/await trong JavaScript/Python là concurrency pattern, không phải parallelism.

IPC là các cơ chế để các process độc lập trao đổi dữ liệu, vì chúng không chia sẻ bộ nhớ trực tiếp.

  • Pipe (anonymous pipe): luồng dữ liệu một chiều giữa parent-child process.
  • Named pipe (FIFO): pipe có tên trong filesystem, không cần parent-child relationship.
  • Shared Memory: vùng nhớ được map vào address space của nhiều process — nhanh nhất vì không copy data, nhưng cần đồng bộ (mutex/semaphore).
  • Message Queue: process gửi/nhận messages qua kernel queue (POSIX mqueue, System V) — asynchronous, có thể buffer.
  • Socket: Unix domain socket (local) hoặc TCP/UDP socket (network) — linh hoạt nhất, dùng cả local và remote; microservices giao tiếp qua HTTP/gRPC là IPC qua socket.
  • Signal: notification đơn giản (SIGTERM, SIGKILL, SIGUSR1) — limited data.
  • Memory-mapped file (mmap): file được map vào memory, nhiều process cùng đọc/ghi — Node.js dùng cho large file processing.

Thực tế: Nginx worker processes giao tiếp với master qua pipe và shared memory.

Deadlock xảy ra khi nhiều process chờ nhau giải phóng resource mà không process nào có thể tiếp tục.

Coffman

  1. xác định 4 điều kiện cần thiết đồng thời:
  2. Mutual Exclusion: resource chỉ 1 process dùng tại một thời điểm
  3. Hold and Wait: process giữ ít nhất 1 resource và chờ thêm resource khác
  4. No Preemption: resource không thể bị lấy bắt buộc
  5. Circular Wait: tồn tại chuỗi vòng P1→P2→...→Pn→P1 chờ nhau

Phòng tránh bằng cách phá vỡ một điều kiện: Lock Ordering (phá Circular Wait): luôn acquire lock theo thứ tự cố định — ví dụ database luôn lock bảng A trước bảng B. Trylock với timeout (phá Hold and Wait): nếu không lấy được lock sau timeout thì release tất cả và retry. Banker's Algorithm: OS kiểm tra trước khi cấp resource. Thực tế: Java synchronized blocks, Go channel patterns, và database transaction isolation level đều phải xử lý deadlock — PostgreSQL phát hiện deadlock và rollback một transaction.

Mutex (Mutual Exclusion Lock) là binary lock: chỉ có 2 trạng thái locked/unlocked, và quan trọng là chỉ thread nào lock thì mới được unlock (ownership semantics). Dùng để bảo vệ critical section — chỉ 1 thread access vào một lúc.

Semaphore là counter: counting semaphore cho phép N thread cùng access (semaphore=N); binary semaphore (N=1) tương tự mutex nhưng không có ownership — thread A có thể signal semaphore mà thread B đã wait.

Dùng mutex khi: bảo vệ shared data (HashMap, linked list) — một thread đọc/ghi tại một thời điểm. Dùng semaphore khi: giới hạn concurrent access (connection pool tối đa 10 connections), producer-consumer synchronization (producer signal khi có item mới). Deadlock risk: mutex thường dùng recursive mutex để tránh self-deadlock. Go dùng sync.Mutexsync.RWMutex (nhiều reader, 1 writer); channel thường là cách idiomatic hơn để synchronize trong Go.

Thread pool là tập hợp các worker threads được tạo sẵn và tái sử dụng, thay vì tạo/hủy thread mới cho mỗi task — vì tạo thread tốn ~1MB stack, time để OS allocate, và context switch overhead.

  • Hoạt động: task queue nhận công việc, worker thread nhàn rỗi lấy task từ queue, thực thi, rồi chờ task tiếp theo.
  • ThreadPoolExecutor (Java), worker_threads pool (Node.js), Goroutine worker pattern (Go).
  • Sizing là bài toán quan trọng: với CPU-bound tasks, pool size = số CPU cores (thêm thread không giúp, chỉ tốn context switch); với I/O-bound tasks, pool size lớn hơn số CPU (vì thread chờ I/O không chiếm CPU) — formula tham khảo: threads = cores * (1 + wait_time/service_time).
  • Pool quá nhỏ: task queue dài, high latency.
  • Pool quá lớn: memory tốn kém, nhiều context switch, performance giảm.
  • Java Tomcat default pool size 200; Node.js libuv I/O thread pool default 4 (UV_THREADPOOL_SIZE).

Virtual memory là abstraction layer giữa process và RAM vật lý: mỗi process có không gian địa chỉ ảo riêng (64-bit: 128TB), OS + MMU (Memory Management Unit) dịch virtual address sang physical address qua page table.

Lợi ích:

  1. Isolation: process A không thể đọc bộ nhớ process B dù cùng máy
  2. Overcommit: tổng virtual memory của tất cả process có thể vượt RAM thực (OS swap ít-dùng pages ra disk)
  3. Shared libraries: nhiều process share cùng physical pages của libc nhưng map vào virtual address space riêng
  4. Memory-mapped files (mmap): file map vào virtual memory, OS lazily load pages khi access
  5. Copy-on-Write (CoW) khi fork: parent và child share physical pages cho đến khi một bên write

Nhược điểm: page table overhead (~8MB per process với 4-level paging); TLB miss latency. Thực tế: docker stats show virtual memory rất lớn nhưng RSS (Resident Set Size) là RAM thực dùng; OOM Killer kill process dùng nhiều RAM nhất khi system hết memory.

Paging chia physical memory thành các frame kích thước cố định (4KB, 2MB, 1GB huge pages), và virtual memory thành pages cùng kích thước — OS map pages lên frames qua page table. Fragmentation chỉ là internal (page cuối có thể dư), không có external fragmentation. Segmentation chia memory thành các segment logic kích thước khác nhau (code, data, stack, heap) — phản ánh structure của program; có external fragmentation vì segment size biến động.

x86-64 trên thực tế dùng paging là primary mechanism: 4-level page table (PML4→PDPT→PD→PT) với page size 4KB mặc định; segmentation vẫn tồn tại nhưng flat segmentation (base=0, limit=max) — essentially disabled. Linux dùng huge pages (2MB/1GB) để giảm TLB pressure cho database workloads: PostgreSQL và Redis benefit từ transparent huge pages. Thực tế lập trình: stack overflow = page fault khi access stack guard page; segfault = access unmapped virtual address.

Memory leak xảy ra khi program allocate memory nhưng không release, dần dần RSS tăng cho đến khi OOM.

Nguyên nhân phổ biến:
- global/module-level variables tích lũy data.
- event listeners không được removeListener.
- closures capture large objects.
- circular references (trong ngôn ngữ reference-counted).
- unbounded cache/map.
- timer setInterval không clearInterval.

Trong Node.js: dùng node --inspect + Chrome DevTools Memory tab để heap snapshot và so sánh; clinic.js heapprofiler cho production; process.memoryUsage().heapUsed monitor; WeakMap/WeakRef cho cache để GC tự thu dọn khi key không còn reference.

Trong Go: goroutine leak (goroutine blocked trên channel mãi mãi, không bao giờ exit) là phổ biến hơn memory leak; dùng pprof heap/goroutine profiler; runtime.ReadMemStats để monitor.

Best practice: giới hạn size của in-memory cache; dùng context cancellation để goroutines tự cleanup; integration test monitor memory growth theo thời gian.

File system quản lý cách data được lưu trên storage: cấu trúc directory tree, metadata (inode: permissions/timestamps/size/data block pointers), free space management, journaling để tránh corruption khi crash.

ext4 (Linux): journaling filesystem, extents thay vì block maps, delayed allocation, dir_index (HTree), max file 16TB, max volume 1EB — workhorse của Linux servers, stable và battle-tested. NTFS (Windows): journaling, Master File Table (MFT), support NTFS permissions/ACL, Alternate Data Streams, compression/encryption built-in, max 16EB — nhưng Linux support read/write NTFS qua ntfs-3g/ntfs3. APFS (Apple): Copy-on-Write (không corrupt khi crash), native encryption, snapshots (Time Machine), clones (copy-on-write files — cp instant), space sharing giữa volumes trong container — optimized cho SSD/Flash.

Thực tế DevOps: Docker image layers dùng overlay2 filesystem; ZFS (FreeBSD/Linux) thêm checksums và RAID tích hợp cho NAS; volume mount giữa host và container cần match permissions.

Signal là cơ chế notification asynchronous mà OS hoặc process khác gửi đến process: SIGTERM (15, terminate request — default cho kill PID), SIGKILL (9, force kill — không thể catch/ignore), SIGINT (2, Ctrl+C), SIGHUP (1, terminal closed/reload config), SIGUSR1/SIGUSR2 (user-defined, Node.js dùng SIGUSR1 để enable debugger), SIGCHLD (child process terminated), SIGPIPE (write to broken pipe).

Graceful shutdown là critical trong production: khi nhận SIGTERM, server stop accepting new connections, drain in-flight requests, close DB connections, flush logs — rồi mới exit. Node.js graceful shutdown: process.on('SIGTERM', async () => { await server.close(); await db.disconnect(); process.exit(0); }).

Kubernetes gửi SIGTERM trước khi terminationGracePeriodSeconds (default 30s), sau đó gửi SIGKILL — phải handle SIGTERM để zero-downtime deploy. Go: signal.NotifyContext(ctx, syscall.SIGTERM) để propagate cancellation đến tất cả goroutines khi nhận signal.

Race condition xảy ra khi kết quả của chương trình phụ thuộc vào thứ tự/timing của các concurrent operations — thứ tự không được control → kết quả unpredictable. Ví dụ kinh điển (balance):

text
Thread 1: read balance=100, Thread 2: read balance=100
Thread 1: balance += 50  150, Thread 2: balance -= 30  70
Thread 1: write 150, Thread 2: write 70 (lost update!)
Expected: 100 + 50 - 30 = 120

Detect: Go race detector (go test -race, go run -race); Java ThreadSanitizer; Helgrind (Valgrind tool); code review tìm shared mutable state không có synchronization. Ngăn chặn:

  1. Mutex/lock bảo vệ critical section;
  2. Atomic operations (sync/atomic Go, java.util.concurrent.atomic);
  3. Immutable data — không thể race nếu không ai write;
  4. Message passing thay vì shared state (Go channels: "Don't communicate by sharing memory, share memory by communicating");
  5. Database-level: optimistic locking (version column) hoặc pessimistic locking (SELECT FOR UPDATE);
  6. CAS (Compare-And-Swap) cho lock-free algorithms

Monitor là high-level synchronization construct: object chứa mutex (đảm bảo mutual exclusion) + condition variables (cho phép threads wait/notify). Producer-Consumer problem: producer thêm item vào bounded buffer, consumer lấy ra; khi buffer đầy producer phải chờ; khi buffer rỗng consumer phải chờ. Implement với Java (built-in monitor):

java
synchronized void produce(Item item) {
    while (buffer.isFull()) wait(); // release lock, wait
    buffer.add(item);
    notifyAll(); // wake up waiting consumers
}
synchronized Item consume() {
    while (buffer.isEmpty()) wait();
    Item item = buffer.remove();
    notifyAll(); // wake up waiting producers
    return item;
}

Go implementation thường dùng channel:

go
ch := make(chan Item, bufferSize)
// Producer
ch <- item
// Consumer
item := <-ch

Go channel built-in implement producer-consumer pattern với blocking semantics. Semaphore approach: dùng counting semaphore empty (=N) và full (=0) + mutex — classic Dijkstra solution. Java BlockingQueue: production-ready implementation.

select/poll là O(n) scan không scale; epoll (Linux) và kqueue (BSD) dùng kernel-managed event notification, O(1) per event, scale đến hàng triệu FDs; io_uring là next-gen zero-copy async I/O.

select (POSIX): monitor tối đa 1024 FDs (FD_SETSIZE), truyền full set mỗi syscall (O(n) scan), không biết FD nào ready phải scan toàn bộ, không scale. poll: tương tự select nhưng không giới hạn 1024 FDs (dùng dynamic array), vẫn O(n) scan. epoll (Linux 2.6+): kernel-side interest list — epoll_create, epoll_ctl(ADD/MOD/DEL), epoll_wait chỉ trả về ready FDs (O(1) per event). Edge-triggered (ET) mode: notify chỉ khi state change → must drain toàn bộ; Level-triggered (LT, default): notify khi FD vẫn readable/writable → simpler. kqueue (FreeBSD/macOS): tổng quát hơn epoll — kevent struct monitor FDs, signals, timers, processes, sockets; kevent() syscall combine register và wait. Không phải chỉ network — có thể monitor file changes (không cần inotify riêng). io_uring (Linux 5.1+): next-gen async I/O — submission queue + completion queue, zero-copy, batch syscalls, giảm context switch drastically. Đang được adopt bởi Rust tokio, Node.js 18+.

MLFQ ưu tiên interactive processes bằng cách tự động hạ priority của CPU-bound tasks; Linux CFS thay thế MLFQ bằng vruntime tracking để đảm bảo công bằng mà vẫn ưu tiên interactive.

MLFQ: nhiều queue với priority khác nhau (Q0 highest priority, Q_n lowest). Rules: process mới vào Q0 (highest); nếu dùng hết time quantum → demote xuống queue thấp hơn (CPU-bound); nếu yield trước hết quantum (I/O-bound) → giữ hoặc promote; priority boost định kỳ (tất cả lên Q0) để tránh starvation. Kết quả: interactive processes (editor, browser) luôn responsive; CPU-bound background tasks chạy khi không có interactive job. Linux CFS (Completely Fair Scheduler): không dùng fixed time quantum — track vruntime (virtual runtime) của mỗi process, chọn process có vruntime nhỏ nhất để chạy tiếp. Process I/O-bound có vruntime nhỏ (ít chạy) → được ưu tiên cao khi wakeup. Nice value (-20 đến +19) tăng/giảm vruntime growth rate. Real-time scheduling: SCHED_FIFO, SCHED_RR cho realtime processes — preempt CFS processes. Practical: nice, renice command điều chỉnh priority; chrt set realtime policy; container CPU limits dùng CFS bandwidth controller (cpu.cfs_quota_us).

File descriptor là abstraction cốt lõi của Linux I/O; pipe là unidirectional IPC qua kernel buffer; /proc là virtual FS cho phép introspect kernel state — ba concept liên kết chặt với nhau.

File Descriptor (FD): là integer đại diện cho open file/socket/pipe/device. Process bắt đầu với 3 FDs: 0 (stdin), 1 (stdout), 2 (stderr). ulimit -n show max FDs per process (default 1024, production nên tăng lên 65536+). Leak FD: process mở file/socket nhưng không close → FD exhaustion → Too many open files. Check: lsof -p PID | wc -l. Pipe: anonymous pipe | trong shell — kernel buffer (thường 65536 bytes), unidirectional, một đầu write (stdout của process A), một đầu read (stdin của process B). Named pipe (FIFO): tồn tại trong filesystem, không cần parent-child. /proc filesystem: virtual filesystem kernel expose information: /proc/PID/fd/ (FDs của process), /proc/PID/maps (memory mapping), /proc/PID/status (process info), /proc/meminfo (RAM usage), /proc/cpuinfo, /proc/net/tcp (TCP connections). Mọi thứ là file — cat /proc/1/cmdline là command của init process. Docker stats đọc từ /proc và cgroups.

Linux process có 5 trạng thái chính: R (running), S (interruptible sleep), D (uninterruptible — không thể kill), Z (zombie), T (stopped) — trạng thái D là dấu hiệu storage issue nghiêm trọng.

Xem process state bằng ps aux hoặc top (cột STATUS/S):
- R (Running/Runnable): đang chạy trên CPU hoặc trong run queue chờ CPU
- S (Sleeping/Interruptible sleep): đang wait (I/O, event, timer) nhưng có thể bị interrupt bởi signal — phổ biến nhất
- D (Disk sleep/Uninterruptible sleep): đang wait I/O không thể interrupt — kernel dùng để bảo vệ critical disk I/O operation. Process ở trạng thái D không respond SIGTERM/SIGKILL — không thể kill! Phổ biến khi NFS mount bị lỗi, disk hang, hoặc storage timeout. Quá nhiều D processes là dấu hiệu storage subsystem có vấn đề
- Z (Zombie): process đã exit nhưng parent chưa wait() để thu dọn. Process zombie không tốn CPU/memory nhưng tốn PID slot. Dấu hiệu parent không handle SIGCHLD
- T (Stopped): bị dừng bởi signal (SIGSTOP, SIGTSTP = Ctrl+Z) hoặc debugger. Resume bằng SIGCONT
- I: Idle kernel thread (không phải running, không phải sleeping)

Debug: ps aux | grep ' D ' tìm D processes; strace -p PID xem syscall nào đang block.

Khi process A gọi fork(), OS tạo process B là bản copy của A. Nếu copy toàn bộ memory ngay lập tức thì tốn kém và lãng phí (nhất là khi child chỉ exec một program khác sau fork). Copy-on-Write (CoW): sau fork, parent và child share cùng physical memory pages, đều chỉ có read-only mapping. Khi một bên (thường là child) write vào page → hardware fault → OS copy chỉ page đó ra một bản riêng → write vào bản copy. Pages không bị write thì không bao giờ copy — save memory và time.

Tại sao quan trọng với Redis RDB snapshot: khi Redis fork một child process để ghi RDB snapshot ra disk, child và parent share toàn bộ memory via CoW. Child đọc data để serialize (không write). Parent tiếp tục serve requests và write. Mỗi page parent write → copy của page đó. Nếu Redis đang write nhiều trong khi snapshot → nhiều page bị copy → memory tăng đột biến (used_memory + used_memory_rss diverge).

Monitoring: rdb_last_bgsave_time_sec, latest_fork_usec, Redis INFO stats. Tip: Redis fork nhanh nhất khi không có memory fragmentation và THP disabled.

Inode chứa metadata file (không phải tên); hard link là alias đến cùng inode; symlink là pointer đến path — khác nhau về behavior khi target bị xóa và khi cross-filesystem.

Inode (Index Node): metadata của file trong filesystem — chứa permissions, owner, timestamps, file size, block pointers (địa chỉ data blocks trên disk). KHÔNG chứa filename! Filename chỉ là entry trong directory trỏ đến inode number. stat file.txt hiển thị inode info. df -i xem inode usage (có thể hết inodes dù còn disk space). Hard link: tạo thêm directory entry trỏ đến cùng inode. ln file.txt hardlink.txt. Cả hai file là same data, same inode, same permissions. Xóa một file không xóa data — data chỉ xóa khi link count = 0 (tất cả hard links bị xóa). Không thể hard link qua filesystem khác hay directory. Symbolic link (symlink): là special file chứa path đến target. ln -s /path/to/target symlink. Symlink có inode riêng. Nếu target bị xóa → dangling symlink. Có thể symlink qua filesystem và directory. Ứng dụng: /etc/nginx/sites-enabled/ chứa symlinks đến /etc/nginx/sites-available/ — enable/disable site bằng cách tạo/xóa symlink. current symlink → deployment rollback.

Linux page cache là phần RAM kernel dùng để cache file content từ disk — khi đọc file lần đầu, kernel load data vào page cache; lần sau đọc cùng file sẽ phục vụ từ RAM (không động đến disk). Write-back: write vào file chỉ update page cache (dirty pages), kernel flush xuống disk theo định kỳ hoặc khi fsync() được gọi.

Vì vậy write thường nhanh (vào RAM), nhưng data chưa an toàn trên disk cho đến khi flush. Tại sao I/O nhanh: vì page cache absorb phần lớn I/O — database với shared_buffers nhỏ vẫn nhanh vì OS page cache bù đắp. free -h hiển thị buff/cache = page cache size. vmstat bi/bo = block in/out (disk I/O thực sự). Drop cache để benchmark thực sự:

bash
sync; echo 3 > /proc/sys/vm/drop_caches

write-back vs write-through: write-back (default) = nhanh nhưng có thể mất data nếu power fail; write-through (O_SYNC hoặc fsync) = chậm hơn nhưng durable.

Database (PostgreSQL, MySQL) tự gọi fsync tại write-ahead log để đảm bảo durability.