Process lifecycle: fork, exec, zombie và systemd
Bắt đầu chương Linux nhé. order-service mình đang chạy trên một VM Ubuntu bằng lệnh nohup java -jar app.jar &. Sáng nay VM reboot là app không tự lên lại. Gõ java -jar thì Linux làm gì bên dưới, và chạy app Java "đúng cách" trên server là thế nào?
Bài 3.1 đã nói process khác thread thế nào. Bài này nhìn từ phía Linux: một process được sinh ra, sống, chết ra sao, và ai lo cho nó khi bạn logout hoặc VM reboot.
1. Bài toán
ssh ubuntu@order-vm
nohup java -Xmx1g -jar /opt/order/app.jar > app.log 2>&1 &
exit
Chạy được vài tuần. Rồi:
- VM reboot để vá kernel → app không tự chạy lại.
- App chết vì OOM lúc 3 giờ sáng → không ai restart.
- Deploy bản mới: phải
ps aux | grep java,killbằng tay, đôi khi kill nhầm PID.
2. Vấn đề
nohup ... & chỉ giúp process không chết khi bạn đóng SSH. Không có ai giám sát nó. Để hiểu vì sao, cần biết process được tạo ra thế nào.
3. Giải pháp: hiểu vòng đời process
fork + exec. Linux tạo process mới bằng hai bước:
fork(): shell (bash) nhân bản chính nó thành process con. Con có PID mới, PPID = PID của bash.exec(): process con thay toàn bộ chương trình của nó bằng/usr/bin/java. PID giữ nguyên.
Mọi process đều có cha. Gốc của cây là PID 1 (systemd trên Ubuntu).
Trạng thái process (cột STAT trong ps, cột S trong top):
| Ký hiệu | Tên | Nghĩa | Ví dụ với order-service |
|---|---|---|---|
| R | Running / Runnable | Đang chạy trên CPU hoặc đang chờ tới lượt CPU | Thread đang tính toán JSON |
| S | Interruptible sleep | Ngủ chờ sự kiện, có thể bị signal đánh thức | Thread Tomcat chờ request, chờ socket DB |
| D | Uninterruptible sleep | Chờ I/O ở tầng kernel, kill -9 cũng không làm gì được ngay | Ghi log lên NFS bị treo, disk chậm |
| Z | Zombie | Đã chết, chờ cha đọc exit code | Script con do app gọi qua ProcessBuilder mà không waitFor() |
| T | Stopped | Bị dừng bằng SIGSTOP / Ctrl+Z | Lỡ bấm Ctrl+Z trong terminal |
Zombie và orphan:
- Zombie: con chết rồi, nhưng cha chưa gọi
wait()để đọc exit code. Kernel giữ lại một dòng trong bảng process. Không tốn RAM/CPU, nhưng tốn PID. Quá nhiều zombie → hết PID → không tạo được process/thread mới. - Orphan: cha chết trước con. Con được "nhận nuôi" bởi PID 1 (hoặc một subreaper). PID 1 sẽ
wait()và dọn nó.
systemd: PID 1 của Ubuntu, đồng thời là service manager. Bạn mô tả app trong một unit file, systemd lo khởi động lúc boot, restart khi chết, gom log, dừng đúng cách.
4. Code từng bước
Bước 1: Xem process hiện tại
ps -eo pid,ppid,user,stat,rss,etime,cmd | grep [j]ava
PID PPID USER STAT RSS ELAPSED CMD
4123 1 ubuntu Sl 812340 12-03:11:20 java -Xmx1g -jar /opt/order/app.jar
PPID = 1: bash cha đã thoát khi bạn logout, process thành orphan và được systemd nhận nuôi. Sl = sleep + multi-threaded.
pstree -p 1 | head # xem cây process
top # realtime, nhấn P sắp theo CPU, M theo RAM
htop # giống top nhưng dễ nhìn, F5 xem dạng cây
Bước 2: Tạo user riêng và unit file
sudo useradd --system --no-create-home --shell /usr/sbin/nologin order
sudo chown -R order:order /opt/order
# /etc/systemd/system/order-service.service
[Unit]
Description=Order Service (Spring Boot)
After=network-online.target
Wants=network-online.target
[Service]
User=order
WorkingDirectory=/opt/order
EnvironmentFile=/etc/order/order.env # DB_PASSWORD=..., quyền 600
ExecStart=/usr/bin/java -Xmx1g -jar /opt/order/app.jar
SuccessExitStatus=143 # JVM thoát do SIGTERM = 128+15
Restart=on-failure
RestartSec=5
TimeoutStopSec=30 # > spring.lifecycle.timeout-per-shutdown-phase
LimitNOFILE=65536 # xem bài 10.4
[Install]
WantedBy=multi-user.target
Bước 3: application.yml đọc secret từ env
spring:
datasource:
url: jdbc:postgresql://${DB_HOST}:5432/orders
username: ${DB_USER}
password: ${DB_PASSWORD}
server:
shutdown: graceful
Bước 4: Bật, kiểm tra, xem log
sudo systemctl daemon-reload
sudo systemctl enable --now order-service
systemctl status order-service
journalctl -u order-service -f # log stdout/stderr của app
curl -s localhost:8080/actuator/health
sudo kill -9 $(systemctl show -p MainPID --value order-service)
sleep 6; systemctl status order-service # đã tự restart
5. Cách hoạt động
systemd (PID 1)
│ fork()
├──► con (PID 4123) ── setuid(order), set env, set limit
│ │ exec("/usr/bin/java ...")
│ ▼
│ java (PID 4123) ── R/S ... chạy hàng ngày
│ │ exit / bị kill
│ ▼
│ Z (zombie) ── chờ cha đọc exit code
│ wait() ◄────────┘
│ đọc exit code: 137? → Restart=on-failure → fork lại sau 5s
▼
6. So sánh
| nohup … & | systemd service | Container (Docker/K8s) | |
|---|---|---|---|
| Tự chạy khi boot | Không | Có (enable) | Có (restart policy) |
| Restart khi chết | Không | Restart=on-failure | Có |
| Log | File tự quản | journald | docker logs |
| Dừng đúng cách | Tự kill | SIGTERM → đợi → SIGKILL | Giống systemd (16.6) |
| Giới hạn tài nguyên | Không | MemoryMax, CPUQuota (cgroups) | cgroups |
7. Khi nào nên dùng / không nên dùng
- App chạy thẳng trên VM: luôn dùng systemd, không dùng
nohup. - Đã chạy trong container/K8s: không cần systemd bên trong container; runtime lo việc giám sát.
nohup/screenchỉ dùng cho việc tạm thời như chạy một script migrate.
top báo order-service dùng RSS 1.6GB trong khi -Xmx1g. Bộ nhớ của một process trên Linux thực chất là gì? Bài 10.2.Mình thấy ps có vài process [sh] <defunct> mà kill -9 không mất. Đó là zombie à? Xử lý sao?
Đúng rồi, <defunct> chính là zombie. Nó đã chết, nên kill -9 không có tác dụng: không có gì để giết nữa. Thứ còn lại chỉ là exit code chờ cha đọc.
Tìm cha của nó:
ps -eo pid,ppid,stat,cmd | awk '$3 ~ /Z/'
5501 4123 Z [sh] <defunct>
# PPID 4123 = order-service
Nghĩa là code Java gọi script mà không đợi kết quả:
// Sai: không waitFor, không đọc output
new ProcessBuilder("sh", "-c", "gzip /data/export.csv").start();
// Đúng
Process p = new ProcessBuilder("gzip", "/data/export.csv")
.redirectErrorStream(true)
.redirectOutput(ProcessBuilder.Redirect.DISCARD)
.start();
if (!p.waitFor(30, TimeUnit.SECONDS)) {
p.destroyForcibly();
}
int code = p.exitValue();
Cách dọn: sửa cha để nó wait(). Nếu chưa sửa được, restart cha: khi cha chết, zombie thành orphan, PID 1 nhận nuôi và dọn ngay. Trong container, PID 1 là java thì không ai dọn, nên mới cần tini / --init (xem 16.6).
Process ở trạng thái D thì sao? Mình thấy lúc disk chậm, systemctl stop treo mãi.
Bạn quan sát đúng. Process ở D đang nằm trong kernel chờ I/O (thường là disk hoặc NFS). Kernel không cho signal cắt ngang lúc đó, kể cả SIGKILL. Signal được "xếp hàng" và chỉ có hiệu lực khi I/O trả về.
ps -eLo pid,tid,stat,wchan:30,comm | awk '$3 ~ /D/'
4123 4188 D io_schedule java
- Nhiều thread ở D → load average tăng dù CPU rảnh (bài 10.3).
- Cách xử lý thật: tìm nguyên nhân I/O (
iostat -x 1,dmesgbáo lỗi disk/NFS), không phải kill mạnh hơn. - Thường gặp với Java: ghi log đồng bộ lên volume mạng. Dùng async appender và log ra disk local / stdout.
Phỏng vấn hỏi vòng đời process trên Linux thì trả lời sao?
Q1. Linux tạo process mới thế nào?
🇻🇳 Tiếng Việt:
Bằng fork rồi exec. Fork nhân bản process cha thành con với PID mới; exec thay chương trình trong process con bằng chương trình khác, ví dụ java, giữ nguyên PID. Mọi process đều có cha, gốc là PID 1, trên Ubuntu là systemd.
🇬🇧 English:
With fork followed by exec. Fork duplicates the parent into a child with a new PID; exec replaces the program in that child with another one, for example java, keeping the same PID. Every process has a parent, and the root is PID 1, which is systemd on Ubuntu.
Q2. Zombie và orphan process khác nhau thế nào?
🇻🇳 Tiếng Việt:
Zombie là process đã chết nhưng cha chưa gọi wait để đọc exit code, nó chỉ chiếm một PID và không kill được. Orphan là process còn sống nhưng cha đã chết, nó được PID 1 nhận nuôi. Cách sửa zombie là sửa cha cho nó wait, hoặc restart cha để PID 1 dọn.
🇬🇧 English:
A zombie has already exited but its parent has not called wait to collect the exit code, so it only holds a PID and cannot be killed. An orphan is still running but its parent has died, so it is adopted by PID 1. To fix zombies you fix the parent to call wait, or restart the parent so PID 1 reaps them.
Q3. Chạy một Spring Boot app trên VM Linux trên production thế nào?
🇻🇳 Tiếng Việt:
Em viết một systemd unit: chạy bằng user riêng, đọc secret từ EnvironmentFile, Restart=on-failure, TimeoutStopSec lớn hơn thời gian graceful shutdown của Spring, đặt LimitNOFILE, rồi enable để tự chạy khi boot. Log đọc bằng journalctl. Không dùng nohup vì không có ai giám sát process.
🇬🇧 English:
I write a systemd unit: a dedicated user, secrets from an EnvironmentFile, Restart=on-failure, a TimeoutStopSec longer than Spring's graceful shutdown, a LimitNOFILE setting, and I enable it so it starts on boot. Logs are read with journalctl. I avoid nohup because nothing supervises the process.
Cách nhớ ngắn gọn
- fork = nhân bản, exec = thay chương trình, PID giữ nguyên.
- R chạy, S ngủ chờ, D chờ I/O không kill được, Z đã chết chờ cha.
- Zombie: sửa cha. Orphan: PID 1 nhận nuôi.
- Trên VM: systemd, không nohup.
Tự kiểm tra
Sau exec, PID của process có đổi không?
Không. exec chỉ thay chương trình bên trong, PID giữ nguyên.
Vì sao kill -9 không xóa được zombie?
Zombie đã chết. Chỉ còn exit code chờ cha wait(); phải sửa hoặc restart cha.
Process ở trạng thái D nghĩa là gì?
Đang chờ I/O trong kernel, không bị signal cắt ngang. Thường do disk/NFS chậm.
Vì sao unit file có SuccessExitStatus=143?
JVM thoát do SIGTERM trả exit code 143 (128+15). Khai báo để systemd coi đó là dừng bình thường.
TimeoutStopSec phải so với gì?
Phải lớn hơn spring.lifecycle.timeout-per-shutdown-phase, nếu không systemd SIGKILL giữa chừng.