Process lifecycle: fork, exec, zombie và systemd

4 câu hỏi~25 phútTừ khóa: fork, exec, PID, PPID, R/S/D/Z, zombie, orphan, ps, top, htop, systemd, unit file, Restart=on-failure

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:

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:

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ệuTênNghĩaVí dụ với order-service
RRunning / RunnableĐang chạy trên CPU hoặc đang chờ tới lượt CPUThread đang tính toán JSON
SInterruptible sleepNgủ chờ sự kiện, có thể bị signal đánh thứcThread Tomcat chờ request, chờ socket DB
DUninterruptible sleepChờ I/O ở tầng kernel, kill -9 cũng không làm gì được ngayGhi log lên NFS bị treo, disk chậm
ZZombieĐã chết, chờ cha đọc exit codeScript con do app gọi qua ProcessBuilder mà không waitFor()
TStoppedBị dừng bằng SIGSTOP / Ctrl+ZLỡ bấm Ctrl+Z trong terminal

Zombie và orphan:

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 serviceContainer (Docker/K8s)
Tự chạy khi bootKhôngCó (enable)Có (restart policy)
Restart khi chếtKhôngRestart=on-failureCó
LogFile tự quảnjournalddocker logs
Dừng đúng cáchTự killSIGTERM → đợi → SIGKILLGiống systemd (16.6)
Giới hạn tài nguyênKhôngMemoryMax, CPUQuota (cgroups)cgroups

7. Khi nào nên dùng / không nên dùng

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

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


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.