Module 0 · Bài 04

Phòng lab: một máy Linux thật ngay trong Windows

Bài 03 cho thấy app là một process do kernel quản lý, nhưng mọi lab của nó chạy trên Windows. Bài này dựng một kernel Linux thật trên chính máy bạn, học vừa đủ shell để điều khiển nó, rồi chạy app mẫu của khoá học trên đó và giết nó bằng hai cách.

Đang học 3-5 giờ học Cần quyền quản trị Windows Phụ lục: Lịch sử WSL Phụ lục: Terminal và shell

Bài 03 kết thúc ở một câu hỏi: Server thật chạy Linux, còn mọi lab tới giờ chạy trên Windows. Làm sao có một máy Linux thật ngay trên máy mình, để thấy process, signal và lỗi đúng như trên server? (xem lại đoạn cuối Bài 03).

Vì sao lại có câu hỏi này? Vì lab của Bài 03 chạy trên Windows, nên có mấy chỗ chỉ thấy được thứ gần giống, chưa thấy được đồ thật.

Tình huống

Làm xong lab Bài 03 trong Git Bash, đọc lại thì kết quả nào cũng kèm một chữ "nhưng".

Bấm Ctrl+C trên Windows thì không có signal nào được gửi đi. Windows chỉ báo một sự kiện cho cửa sổ dòng lệnh, rồi Node tự dịch sự kiện đó thành SIGINT. Lệnh taskkill //F cũng chỉ gần giống SIGKILL. Còn lệnh mà Bài 03 dặn dùng để tìm dấu vết của OOM killer thì không có luôn:

$ dmesg
bash: dmesg: command not found

Con số tự đo cũng mang dấu của Windows. Gọi một port không ai listen thì mất 2,155 giây mới nhận được refused. Bài 01 lại nói trên Linux lời từ chối tới gần như ngay. Mà server thật thì chạy Linux.

Lý thuyết Bài 03 dạy về signal và OOM killer thì đúng, nhưng thứ bạn tận mắt thấy trong lab mới chỉ là bản gần giống. Bảng 1 gom những chỗ đó lại.

Bảng 1 Những gì lab của Bài 03 chỉ thấy được bản tương đương, vì nó chạy trên Windows.
Thứ cần quan sátTrên Windows, trong Git BashTrên một server Linux
SIGINT khi bấm Ctrl+CSự kiện của cửa sổ dòng lệnh, Node tự dịchKernel gửi SIGINT tới process
SIGTERM, SIGKILLKhông có signal; taskkill //F kết thúc cưỡng bứckill gửi SIGTERM, kill -9 gửi SIGKILL
OOM killer, dmesgKhông cóKernel ghi lại mỗi lần nó giết process
refused ở port không ai listenKhoảng 2 giây, vì Windows thử lạiThường vài mili-giây, chưa đo trên máy này

Dòng cuối của bảng là dòng dễ kiểm nhất, vì chỉ cần chạy lại đúng một lệnh trên một kernel khác. Hình 1 đặt hai lần đo cạnh nhau, cột phải còn để trống.

  Windows · Git Bash                      Linux · bash
  ──────────────────────────────────      ──────────────────────────────────
  $ time curl -4 -o /dev/null \           $ time curl -4 -o /dev/null \
      http://127.0.0.1:9999                   http://127.0.0.1:9999
  curl: (7) Failed to connect …           curl: (7) Failed to connect …
        after 2076 ms                           after ? ms

  real    0m2.155s                        real    ?
Hình 1 Cùng một lệnh, hai kernel. Cột trái là output thật đo ở Bài 01. Cột phải là phép đo đối chứng của bài này, chưa điền được vì chưa có máy Linux nào để chạy.

Muốn điền cột phải thì cần những gì? Cần hai thứ, là một kernel Linux chạy trên máy bạn và một cách ra lệnh cho nó. Có hai thứ đó rồi thì không dừng ở một phép đo: bài này chạy luôn một app thật trên Linux, là app mẫu của khoá học, để thấy tận mắt hai dòng đầu của Bảng 1.

Bài này trả lời ba câu
  1. Máy ảo là gì, và WSL2 đặt một kernel Linux thật vào máy mình bằng cách nào?
  2. Điều khiển máy Linux đó bằng gì: terminal, shell, lệnh, và cách đi lại giữa các thư mục?
  3. App mẫu chạy trên Linux ra sao, bị kill và kill -9 thì để lại gì, và nó còn thiếu gì để làm được việc?

Cần đã học trước

Vì sao không cài Linux lên Windows như cài một phần mềm? Lý do nằm ở hai kết luận của Bài 03. Một là chỉ kernel được chạm phần cứng. Hai là chương trình phải xin kernel làm hộ, qua syscall.

Vì sao phải là chính kernel Linux

Mọi dòng ở cột phải của Bảng 1 đều là việc của kernel Linux. Signal do kernel gửi, OOM killer là một phần của kernel, còn lệnh dmesg thì đọc thông báo do kernel tự ghi. Vì vậy không thể cài mấy thứ đó lên Windows như cài thêm một chương trình. Còn một lý do nữa: chương trình biên dịch cho Linux, như bash hay PostgreSQL, nói chuyện với kernel bằng bộ syscall của Linux, mà kernel của Windows không hiểu bộ syscall đó.

Lúc học tới đây mình bị hỏi cần gì để thấy được OOM killer, và mình trả lời là cần có Linux trên máy. Câu đó thiếu mất một chữ, vì thứ cần là kernel Linux. Git Bash cho thấy rõ chỗ này. Các lệnh trong đó là lệnh Linux được biên dịch lại để chạy trên Windows, nên bên dưới vẫn là kernel Windows. Mình gọi nó là hàng đè tem, chứ không phải Linux thật.

Vậy là cần một kernel Linux. Nhưng Bài 03 còn nói chỉ một kernel được nắm phần cứng, và trên máy bạn đó là kernel Windows, thế thì đặt kernel thứ hai vào đâu? Người ta đã thử ba cách:

  1. Biên dịch lại từng chương trình Linux cho Windows. Git Bash ra đời như vậy.
  2. Dịch từng syscall của Linux sang lời gọi của Windows. Đó là WSL thế hệ đầu.
  3. Chạy hẳn một kernel Linux trong một máy ảo.

Phụ lục Lịch sử WSL kể vì sao hai cách đầu không đủ. Cách thứ ba là cách của WSL2, nên phần dưới đi vào nó.

Máy ảo và hypervisor

Máy ảo virtual machine · VM
Một máy tính dựng bằng phần mềm, nằm trên một máy thật. Nó có CPU, RAM, ổ đĩa, card mạng ảo, và chạy một hệ điều hành đầy đủ có kernel riêng. Kernel đó gọi là kernel khách.
Hypervisor
Lớp phần mềm nằm dưới mọi kernel, chia phần cứng thật cho các máy ảo và giữ chúng cô lập. Hypervisor của Windows tên là Hyper-V.

Hypervisor làm được việc đó nhờ đâu? Nhờ một tính năng của CPU tên là ảo hoá phần cứng (Intel gọi là VT-x, AMD gọi là AMD-V). Khi tính năng này bật, CPU có thêm một mức đặc quyền, cao hơn cả kernel mode của Bài 03, và hypervisor chạy ở mức đó. Kernel khách vẫn chạy ở kernel mode, nhưng chỉ trong phần tài nguyên nó được chia. Mỗi khi nó chạm ra ngoài phần đó, ví dụ tới một thiết bị thật, CPU dừng nó lại và trao quyền cho hypervisor quyết định.

Nhờ vậy hai kernel chạy cùng lúc được trên một máy. Mỗi kernel độc quyền phần của mình, còn hypervisor độc quyền việc chia. Khi Hyper-V bật thì chính Windows cũng chạy bên trên hypervisor, nằm cạnh các máy ảo.

Bạn đã gặp máy ảo ở đâu chưa? Rồi, chỉ là chưa gọi tên. Chiếc VPS ở Bài 03 thường là một máy ảo cắt ra từ một server lớn. Trên máy cá nhân thì VirtualBox hay VMware cho bạn tự dựng máy ảo. VirtualBox đặt hypervisor ở một chỗ khác với Hyper-V, và Ảnh 1 vẽ hai chỗ đó cạnh nhau.

Sơ đồ hai kiểu hypervisor. Bên trái, type 1: phần cứng ở dưới, hypervisor ở giữa, các hệ điều hành ở trên. Bên phải, type 2: phần cứng, rồi một hệ điều hành, rồi hypervisor chạy trên hệ điều hành đó, rồi các hệ điều hành khách ở trên cùng.
Ảnh 1 Hai chỗ đặt hypervisor. Bên trái là kiểu 1 (native, còn gọi bare metal): hypervisor nằm ngay trên phần cứng, mọi hệ điều hành chạy bên trên nó. Microsoft xếp Hyper-V vào kiểu này, nên khi Hyper-V bật, Windows cũng là một hệ điều hành chạy trên hypervisor. Bên phải là kiểu 2 (hosted): hypervisor là một chương trình chạy trên một hệ điều hành có sẵn, và các hệ điều hành khách chạy bên trên chương trình đó. Tài liệu của VirtualBox tự xếp nó vào kiểu 2.

Theo tài liệu của Microsoft, máy ảo truyền thống khởi động chậm, tốn tài nguyên và đòi bạn tự quản lý. WSL2 được thiết kế để bỏ đúng những điểm yếu này.

WSL2: một máy ảo nhẹ do Windows tự quản

Định nghĩa

WSL (Windows Subsystem for Linux) là thành phần của Windows cho phép cài một bản phân phối Linux và chạy chương trình Linux nguyên bản, không sửa đổi.

WSL 2 là thế hệ mặc định hiện nay. Theo tài liệu của Microsoft, nó "dùng công nghệ ảo hoá để chạy một kernel Linux bên trong một máy ảo tiện ích nhẹ". Windows tự khởi động và quản lý máy ảo này, bạn không phải cấu hình nó.

Kernel trong máy ảo đó có phải đồ thật không? Phải, nó là kernel Linux thật. Microsoft biên dịch nó từ mã nguồn trên kernel.org và cập nhật qua Windows Update, nên syscall của Linux được chính kernel Linux trả lời.

Thế hệ trước là WSL 1 thì không có kernel Linux. Nó đón từng syscall của Linux rồi dịch sang lời gọi của kernel Windows, nên syscall nào chưa được dịch thì chương trình cần tới nó không chạy được. Bảng 2 chép lại bảng so sánh hai thế hệ trong tài liệu của Microsoft.

Bảng 2 So sánh tính năng của WSL 1 và WSL 2, chép đủ mười hàng của bảng Comparing features; tên tính năng giữ nguyên tiếng Anh. Nguồn: Microsoft Learn, Comparing WSL Versions, đọc ngày 03/10/2026.
FeatureWSL 1WSL 2
Integration between Windows and Linux
Fast boot times
Small resource foot print compared to traditional Virtual Machines
Runs with current versions of VMware and VirtualBox
Managed VM
Full Linux Kernel
Full system call compatibility
Performance across OS file systems
systemd support
IPv6 support

Ba hàng in đậm là ba hàng bài này dựa vào.

  • Hàng Full Linux Kernel là lý do bài này chọn WSL2. Những gì lab của Bài 03 chỉ thấy bản gần giống (Bảng 1) đều là việc của kernel Linux.
  • Hàng Full system call compatibility nghĩa là chương trình Linux nào cũng chạy được, vì bên trả lời syscall là kernel Linux thật chứ không phải một lớp dịch.
  • Hàng Performance across OS file systems là chỗ WSL 1 hơn: đọc file của Windows từ trong Linux thì WSL2 chậm hơn. Ngay dưới bảng, Microsoft khuyên đặt file dự án cùng phía với công cụ dùng nó.

Còn một hàng nữa dính tới bài. Hàng Managed VM ứng với câu cuối của định nghĩa ở trên, tức Windows tự khởi động và quản lý máy ảo.

Có kernel rồi thì dùng được chưa? Chưa, vì kernel không có chương trình nào để bạn gõ lệnh. Phần còn lại đến từ một distro (Linux distribution, bản phân phối Linux). Distro là kernel Linux cộng một bộ chương trình đi kèm, do một tổ chức đóng gói và duy trì. Bộ đó gồm ba thứ, là shell, các lệnh cơ bản như ls, và trình quản lý gói để cài thêm phần mềm. Shell là chương trình nhận lệnh bạn gõ, tab sau giải thích.

Ubuntu là distro mặc định của WSL và rất phổ biến trên server. Trong WSL2, mọi distro dùng chung kernel của Microsoft trong cùng một máy ảo, nhưng mỗi distro có hệ thống file riêng và tập process riêng.

  Windows                         │  Máy ảo WSL2
                                  │
  Git Bash, PowerShell,           │  distro Ubuntu
  trình duyệt, IDE…               │  bash, ls, nano, apt…
                                  │  ───────────────────────────
  kernel Windows                  │  kernel Linux (Microsoft biên dịch)
  ────────────────────────────────┴──────────────────────────────────
                        hypervisor (Hyper-V)
  ───────────────────────────────────────────────────────────────────
               CPU   ·   RAM   ·   ổ đĩa   ·   card mạng
Hình 2 Hai kernel trên một máy. Hypervisor chia phần cứng; mỗi kernel độc quyền phần của mình. Distro Ubuntu là bộ chương trình chạy trên kernel Linux.

Hình 2 kiểm được bằng hai lệnh. Lệnh đầu chạy trong PowerShell, còn lệnh sau chạy trong Ubuntu, và uname -r in phiên bản của kernel đang chạy.

PS> wsl -l -v
  NAME      STATE      VERSION
* Ubuntu    Running    2

$ uname -r
6.6.87.2-microsoft-standard-WSL2
Hình 3 Hai bằng chứng (output kỳ vọng; số phiên bản trên máy bạn sẽ khác). Đuôi microsoft-standard-WSL2 cho biết kernel do Microsoft biên dịch cho WSL2.

Giới hạn của WSL2

WSL2 có ba giới hạn cần biết.

  • CPU phải hỗ trợ ảo hoá, và ảo hoá phải được bật trong phần cài đặt khởi động của máy (BIOS hoặc UEFI). Thiếu thì việc cài đặt dừng ở lỗi 0x80370102.
  • File lab nên đặt trong thư mục của Linux, theo hàng Performance across OS file systems của Bảng 2.
  • WSL2 là phòng lab, không phải server. Theo tài liệu của Microsoft, khi bạn đóng hết cửa sổ Ubuntu thì máy ảo có thể bị tắt, kể cả lúc còn chương trình chạy nền trong đó.

Trên bản đồ của Bài 00, WSL2 là chỗ dựng lại nửa phía server.

Có máy ảo WSL2 rồi thì ra lệnh cho nó bằng gì? Nó giống server ở Bài 03, không có cửa sổ nào để bấm, nên cách duy nhất để ra lệnh là gõ chữ. Tab này chỉ dạy những lệnh mà bài sẽ gõ tới, và lệnh nào cũng được giải nghĩa ngay ở chỗ nó xuất hiện.

Terminal và shell

Bạn gõ chữ vào đâu, và ai đọc chữ đó? Có hai chương trình chia nhau việc này.

Terminal
Chương trình chỉ làm hai việc: hiện chữ lên màn hình và chuyển phím bạn gõ tới chương trình chạy bên trong nó. Terminal không hiểu lệnh. Windows Terminal hay cửa sổ của Git Bash đều là terminal.
Shell
Chương trình đọc một dòng lệnh, nhờ kernel chạy chương trình tương ứng, đợi nó xong rồi hỏi dòng tiếp. Shell là một process chạy bên trong terminal.
bash Bourne Again SHell
Shell mặc định trên Ubuntu và trên hầu hết server Linux. Git Bash là bash đưa sang Windows.

Vì terminal chỉ chuyển chữ còn shell mới hiểu chữ, cùng một cửa sổ Windows Terminal mở được cả cmd.exe, PowerShell lẫn bash, và mỗi shell hiểu dòng lệnh theo cách riêng của nó. Mọi lệnh từ đây trở đi đều viết cho bash.

Đọc thêm, không bắt buộc

Phụ lục Terminal và shell đi chậm hơn phần này: vì sao lại là hai chương trình, bash làm gì với một dòng lệnh từ lúc bạn bấm Enter, nó tìm chương trình ở đâu, và vì sao cd là một lệnh nằm sẵn trong bash. Bỏ qua phụ lục thì vẫn làm được hết lab của bài.

Một dòng lệnh gồm những gì

Mở Ubuntu ra thì thấy gì đầu tiên? Bash in ra một dấu nhắc (prompt) rồi chờ bạn gõ. Trên Ubuntu dấu nhắc có dạng ten@may:~$. Đọc từ trái sang thì đó là tên user, tên máy, thư mục đang đứng, rồi dấu $ cho user thường (với root thì là #).

Dòng bạn gõ sau dấu nhắc được bash cắt thành các chữ theo dấu cách. Chữ đầu tiên là tên lệnh, tức tên chương trình cần chạy, còn các chữ sau là tham số (argument) đưa cho chương trình đó. Theo quy ước, tham số nào bắt đầu bằng - thì là một tuỳ chọn (option). Ví dụ trong ls -l /etc thì ls là tên lệnh, -l là tuỳ chọn và /etc là tham số.

Bash có hiểu -l nghĩa là gì không? Không. Nó chuyển nguyên chữ đó cho chương trình, và chương trình tự đọc. Giống như khi chạy java -jar app.jar --server.port=9090, chữ --server.port=9090 rơi vào mảng String[] args của hàm main.

Exit status: con số một lệnh để lại

Lệnh chạy xong thì bash biết nó thành hay bại bằng cách nào? Bằng một con số gọi là exit status. Đó là số nguyên mà process để lại khi thoát, 0 là thành công còn khác 0 là thất bại. Trong Java, đó là con số truyền vào System.exit(). Bash cất con số của lệnh vừa chạy vào biến $?, nên gõ echo $? ngay sau một lệnh là đọc được nó.

$ ls /etc/hostname
/etc/hostname
$ echo $?
0
$ ls /khong-co
ls: cannot access '/khong-co': No such file or directory
$ echo $?
2

Con số này đáng học vì tab sau dùng chính nó để biết app chết vì signal nào.

Đường dẫn trên Linux

Đường dẫn trên Linux khác Windows ở đâu? Linux không chia ổ C:, D: như Windows. Nó có một cây thư mục duy nhất, bắt đầu từ thư mục gốc /, và trong WSL thì ổ C: của Windows hiện ra ở /mnt/c. Còn hai chỗ khác nữa. Linux ngăn các phần của đường dẫn bằng / chứ không phải \, và Linux phân biệt hoa thường, nên /etc và /Etc là hai đường dẫn khác nhau.

Đường dẫn bắt đầu bằng / thì tính từ gốc cây, nên đứng ở đâu nó cũng trỏ về một chỗ. Đường dẫn không bắt đầu bằng / thì tính từ thư mục bạn đang đứng, trong đó .. là thư mục cha. Mỗi user có một thư mục riêng gọi là thư mục nhà, trên Ubuntu là /home/<tên-user>, viết tắt là ~.

Bảng 3 gom những lệnh bài này gõ tới.

Bảng 3 Những lệnh bài này dùng.
LệnhLàm gìThường gõ
pwdIn thư mục đang đứngpwd
lsLiệt kê thư mụcls -l chi tiết, ls -a cả file ẩn
cdĐổi thư mục đang đứngcd .., cd ~
mkdirTạo thư mụcmkdir -p a/b
catIn nội dung filecat /etc/hostname
nanoSửa file trong terminalCtrl+O lưu, Ctrl+X thoát
psLiệt kê process đang chạyps -ef mọi process, kèm PID
killGửi signal tới một processkill <PID>, kill -9 <PID>
echoIn các tham sốecho $?

sudo và apt: quyền root và cài phần mềm

Bài 03 kết luận kernel cho phép hay từ chối là dựa trên user của process. User bạn tạo lúc cài Ubuntu là user thường, nên nó bị chặn ở những chỗ chỉ root được vào. Ví dụ là /root, thư mục nhà của root (nó không liên quan tới thư mục gốc /, chỉ trùng chữ):

$ ls /root
ls: cannot open directory '/root': Permission denied

Cần quyền root cho một lệnh thì làm sao? Dùng sudo (superuser do). Nó làm ba việc, là kiểm tra bạn có được phép không, hỏi mật khẩu của chính bạn, rồi sinh một process con chạy lệnh bằng user root, ví dụ sudo ls /root. Theo tài liệu của Microsoft, user tạo lúc cài WSL dùng được sudo. Lúc bạn gõ mật khẩu, màn hình không hiện gì cả.

Giới hạn của sudo thì Bài 03 đã nêu: process chạy bằng root mà có lỗi thì lỗi đó có quyền với cả máy. Vì vậy chỉ thêm sudo vào đúng lệnh cần nó. Truyện tranh ở Ảnh 2 kể lại đúng cơ chế này.

Truyện tranh một khung, hai người que. Người ngồi ghế bành nói: Make me a sandwich. Người đứng đáp: What? Make it yourself. Người ngồi nói lại: Sudo make me a sandwich. Người đứng đáp: Okay.
Ảnh 2 Truyện tranh Sandwich của xkcd, số 149, năm 2006. Cùng một câu lệnh, lần đầu bị từ chối, lần sau có sudo đứng trước thì được làm. Lệnh không đổi, chỉ user chạy nó đổi thành root. Truyện bỏ qua một bước của sudo thật: kiểm tra bạn có được phép không và hỏi mật khẩu trước khi làm.

Việc cần sudo nhiều nhất trong bài này là cài phần mềm. Lệnh dmesg ở tình huống mở bài báo command not found, nghĩa là máy không có chương trình nào tên như vậy. Trên Ubuntu, thiếu chương trình thì cài bằng apt, trình quản lý gói: nó tải phần mềm đã đóng gói sẵn từ kho của Ubuntu về rồi đặt vào đúng chỗ. Cài phần mềm là ghi vào thư mục của hệ thống, nên lệnh có dạng sudo apt install <tên-gói>.

Có kernel Linux và có shell rồi thì chạy gì trên đó? Chạy một app thật, loại app bạn vẫn viết hằng ngày. Tab này chạy nó trên Linux, giết nó bằng hai cách để thấy điều Bài 03 mới kể bằng lời, rồi chỉ ra nó còn thiếu gì.

App mẫu là gì

App mẫu là một API Spring Boot nhỏ để ghi chú, nằm trong thư mục lab/app-mau của repo. Nó chỉ có một file Java, và nó lưu dữ liệu vào PostgreSQL, loại database đã có mặt trên bản đồ của Bài 00. Bảng 4 kể ba đường dẫn của nó.

Bảng 4 Ba đường dẫn của app mẫu. Chỉ đường đầu không cần database.
RequestLàm gìCần database
GET /Trả tên app và PID của nó
GET /ghi-chuĐọc mọi ghi chú
POST /ghi-chuThêm một ghi chú

App được viết để tìm database ở localhost:5432, tức port mặc định của PostgreSQL trên chính máy đang chạy app.

Chạy app trên Linux

Muốn chạy một app Java trên máy Linux mới tinh thì cần gì? Cần Java, và máy chưa có. Ubuntu vừa cài không kèm Java, nên việc đầu tiên là cài nó bằng apt, cùng với Maven để đóng code thành file jar.

sudo apt update
sudo apt install openjdk-21-jdk-headless maven

Lệnh apt update tải về danh sách gói mới nhất, còn apt install mới là lệnh cài. Có Java rồi thì lấy code về, đóng thành file jar và chạy:

cd ~
git clone https://github.com/AnhTuan2111/devops-self-learning.git
cd devops-self-learning/lab/app-mau
mvn -q package
java -jar target/app-mau.jar

Lần đầu mvn package mất vài phút, vì Maven phải tải các thư viện của Spring Boot về. Chạy xong thì cuối log có hai dòng như ở Hình 4.

… appmau.AppMau : Started AppMau in 2.426 seconds (process running for 2.993)
[app-mau] PID = 29264, dang listen port 8080
Hình 4 Hai dòng cuối khi app khởi động xong. Đây là output thật khi mình chạy thử app trên Windows ngày 13/10/2026, đã cắt bớt phần đầu dòng thứ nhất. Trên Ubuntu thì PID và thời gian sẽ khác.

Dòng thứ hai do chính app in ra, và nó nói hai điều đã học ở Bài 03: app là một process có PID, và process đó đang listen port 8080. Cửa sổ này từ giờ bị app chiếm, nên muốn gõ lệnh khác thì mở thêm một cửa sổ Ubuntu thứ hai.

App chạy, nhưng chưa làm được việc

App đã lên thì gọi thử được chưa? Ở cửa sổ thứ hai, gọi đường dẫn không cần database trước:

$ curl http://localhost:8080/
{"pid":29264,"app":"app-mau"}

Đường này trả lời ngay. Còn đường đọc ghi chú thì sao?

$ curl -i http://localhost:8080/ghi-chu
HTTP/1.1 500
Content-Type: application/json
…

{"timestamp":"2026-10-12T18:18:48.615Z","status":500,"error":"Internal Server Error","path":"/ghi-chu"}
Hình 5 App trả 500 cho đường dẫn cần database. Output thật trên máy mình ngày 13/10/2026, bỏ ba dòng header ở giữa.

Mã 500 này do ai viết ra? Theo Bài 02 thì 500 là mã của chính app: request đã vào tới code, và code gặp một chuyện nó không xử lý được. Chuyện đó là gì thì log của app ở cửa sổ thứ nhất nói rõ:

Caused by: org.postgresql.util.PSQLException: Connection to localhost:5432 refused.
Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connections.
Hình 6 Dòng nói lý do trong log của app, output thật trên máy mình ngày 13/10/2026. Dòng gốc là một dòng liền, ở đây ngắt làm hai cho vừa khung.

Chữ refused ở đây chính là refused của Bài 01. App mở kết nối tới localhost:5432, máy còn sống nên trả lời ngay, và câu trả lời là "không", vì không có chương trình nào đang listen port 5432. Nói thẳng ra thì máy này chưa có PostgreSQL. App vẫn sống và vẫn listen 8080, nhưng việc chính của nó là ghi chú thì chưa làm được.

Giết app bằng hai cách

Bài 03 nói SIGTERM thì app được báo trước còn SIGKILL thì không, nhưng lab trên Windows chỉ cho thấy bản gần giống. Giờ có kernel Linux thật thì thử được. Trước hết phải biết PID của app, và ở cửa sổ thứ hai có thể tìm nó bằng ps:

$ ps -ef | grep java
ten   1432   812  3 10:02 pts/0  00:00:07 java -jar target/app-mau.jar

Lệnh ps -ef in mọi process của máy, mỗi process một dòng. Dấu | đưa những dòng đó sang cho grep java, và grep chỉ giữ lại dòng nào có chữ java. Cột thứ hai của dòng còn lại là PID. Output trên là dạng kỳ vọng, nên các con số trên máy bạn sẽ khác.

Có PID rồi thì gửi signal. Lệnh kill không ghi số thì gửi SIGTERM, signal số 15:

# Cửa sổ 2
kill 1432

# Cửa sổ 1, nơi app đang chạy
[app-mau] duoc bao truoc, dang don dep roi thoat
$ echo $?
143

App kịp in một dòng trước khi thoát, vì SIGTERM là signal bắt được: Spring Boot nhận nó, chạy các đoạn dọn dẹp đã đăng ký rồi mới thoát. Giờ chạy lại app, tìm PID mới, và lần này gửi SIGKILL, signal số 9:

# Cửa sổ 2
kill -9 1507

# Cửa sổ 1
Killed
$ echo $?
137

Lần này app không in được dòng nào. Chữ Killed là do bash in ra, sau khi kernel báo cho nó biết process con vừa bị giết. Cả hai output trên là dạng kỳ vọng theo tài liệu của bash.

Hai con số 143 và 137 từ đâu ra? Theo tài liệu của bash, khi một lệnh chết vì signal số N thì exit status của nó là 128 cộng N. SIGTERM là số 15 nên ra 143, còn SIGKILL là số 9 nên ra 137. Vì vậy chỉ cần nhìn exit status là biết một process đã tự thoát hay bị giết, và bị giết bằng signal nào.

Bảng 5 Hai cách giết app mẫu trên Linux.
LệnhSignalApp kịp làm gìExit status
kill <PID>SIGTERM, số 15In dòng dọn dẹp rồi thoát143
kill -9 <PID>SIGKILL, số 9Không kịp làm gì137

Lệnh tên là kill nhưng việc nó làm là gửi signal, và signal nào thì do bạn chọn. Truyện ở Ảnh 3 đùa đúng chỗ đó.

Truyện hai khung. Khung trên: một chim cánh cụt nhe răng chĩa khẩu súng ghi chữ KILL vào một chim cánh cụt khác đang toát mồ hôi. Khung dưới: súng bắn ra một lá cờ nhỏ ghi SIGCONT, kèm tiếng plop; con bị chĩa súng thở phào.
Ảnh 3 Truyện Signals của Daniel Stori, turnoff.us, năm 2020. Khẩu súng ghi KILL bắn ra một lá cờ SIGCONT. SIGCONT là signal bảo một process đang tạm dừng chạy tiếp, nên không ai chết cả. kill không ghi gì thì gửi SIGTERM, kill -9 gửi SIGKILL, còn kill -CONT gửi đúng lá cờ trong truyện.

Sau cả hai lần giết, app nằm im. Không có ai dựng nó dậy, vì nó mới là một process chứ chưa phải một service theo nghĩa của Bài 03: chưa có người trông coi nào đứng sau nó. Muốn nó chạy lại thì bạn phải tự gõ lệnh.

Chạy hai bản cùng lúc

Nếu quên là app đang chạy mà gõ lệnh chạy thêm lần nữa thì sao? Bản thứ hai chết ngay lúc khởi động, với thông báo ở Hình 7.

***************************
APPLICATION FAILED TO START
***************************

Description:

Web server failed to start. Port 8080 was already in use.

Action:

Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.
Hình 7 Bản thứ hai của app không lên được. Output thật trên máy mình ngày 13/10/2026.

Đây là lỗi tranh port đã gặp ở lab của Bài 03, lần này do Spring Boot kể lại bằng lời. Kernel giữ bảng các port đang listen, port 8080 đã thuộc về bản thứ nhất, nên bản thứ hai xin listen thì bị kernel từ chối. Dòng Action cũng chỉ đúng việc cần làm: tìm process đang giữ port rồi dừng nó, và bạn vừa biết cách tìm bằng ps -ef | grep java.

Chỗ kẹt: app cần PostgreSQL

Vậy muốn app ghi được ghi chú thì phải làm gì? Phải có PostgreSQL chạy trên máy này và listen port 5432. Cài nó bằng tay thì gồm những việc sau: cài gói PostgreSQL bằng apt, tạo một user database, đặt mật khẩu, tạo một database, rồi kiểm xem nó có nhận kết nối không. Bạn vừa mất chừng đó công cho Java, giờ lại thêm một lượt cho database.

Chưa hết. Mọi thứ vừa cài chỉ nằm trên máy này. Sang máy khác thì làm lại từ đầu, và hai máy cài ở hai thời điểm sẽ nhận hai phiên bản khác nhau, nên app chạy được ở máy này chưa chắc chạy được ở máy kia. Bài này dừng ở đây và không cài PostgreSQL bằng tay.

Chốt

Trên Linux, kill để lại 143 và app kịp dọn dẹp, còn kill -9 để lại 137 và app không kịp làm gì. App mẫu đã chạy, nhưng nó cần PostgreSQL mà máy chưa có.

Chuẩn bị

Lab đi theo thứ tự của ba tab trước, rồi kết thúc bằng phép đo của Hình 1. Chỗ nào ghi output "kỳ vọng" thì đó là dạng theo tài liệu, còn output thật của bạn thì dán vào notes.md.

Bạn cần hai thứ, là Windows 11 hoặc Windows 10 bản 2004 trở lên, và quyền quản trị. Từ Lab 2 trở đi, mọi lệnh chạy trong Ubuntu.

Lab 1: Cài Ubuntu trên WSL2, tìm bằng chứng của kernel Linux

Mở PowerShell bằng "Run as administrator":

wsl --install -d Ubuntu
wsl -l -v

Vì sao phải ghi -d Ubuntu? Theo tài liệu của Microsoft, wsl --install không kèm gì thì chỉ cài trên máy chưa từng có WSL. Máy đã có WSL thì lệnh đó chỉ in trang hướng dẫn. Cài xong máy có thể đòi khởi động lại, và lần đầu mở Ubuntu thì nó hỏi tên user và mật khẩu cho user Linux mới.

Lệnh wsl -l -v phải cho dòng Ubuntu với cột VERSION là 2. Nếu là 1 thì chạy wsl --set-version Ubuntu 2. Xong thì vào Ubuntu và gõ:

uname -r
sudo dmesg | tail -5

Lệnh đầu cho bằng chứng thứ hai của Hình 3. Lệnh sau là việc Git Bash không làm được ở tình huống mở bài, tức đọc thông báo của kernel, và nó cần sudo vì trên nhiều bản Ubuntu chỉ root được đọc. Phần | tail -5 chỉ giữ lại năm dòng cuối cho khỏi trôi màn hình. Con số trong ngoặc vuông ở đầu mỗi dòng là số giây tính từ lúc kernel khởi động.

Lệnh sudo đầu tiên hỏi mật khẩu, và như mục sudo đã nói, màn hình không hiện gì khi bạn gõ. Gõ nhầm thì xoá thật nhiều rồi gõ lại từ đầu, như Ảnh 4.

Truyện một khung: Typing a password in the terminal when you realize you made a mistake. Một người mặt căng thẳng gõ bàn phím, nói: It is not enough yet! Bên cạnh là một chuỗi chữ BACKSPACE xếp thành cột.
Ảnh 4 Truyện Terminal password typing của Daniel Stori, turnoff.us, năm 2018. Nhân vật gõ mật khẩu trong terminal rồi nhận ra mình gõ nhầm. Vì không thấy đã gõ bao nhiêu ký tự nên cứ bấm Backspace liên tục: "vẫn chưa đủ đâu". Bấm thừa không sao. Xoá hết rồi gõ lại là cách chắc nhất.

Lab 2: Đi lại, đọc và sửa file

pwd
mkdir -p ~/lab04
cd ~/lab04
nano ghi-chu.txt        # gõ hai dòng, Ctrl+O, Enter, Ctrl+X
cat ghi-chu.txt
ls -la

Lệnh pwd đầu tiên in ra /home/<tên-user>. Thư mục ~/lab04 nằm trong hệ thống file của Linux, đúng lời khuyên ở phần giới hạn của WSL2. Lệnh cat phải in lại đúng hai dòng bạn đã gõ.

Tự gây lỗi. Ba lệnh sau, mỗi lệnh hỏng một kiểu, và sau mỗi lệnh gõ thêm echo $? để xem exit status:

cd /Etc
ls /root
sudo ls /root

cd /Etc cho No such file or directory vì Linux phân biệt hoa thường. ls /root cho Permission denied vì bash chạy bằng user thường. Lệnh thứ ba hỏi mật khẩu rồi chạy ls bằng root, và chỉ lệnh này để lại exit status 0.

Lab 3: Chạy app mẫu

Cài Java và Maven, lấy code về rồi chạy, đúng các lệnh ở tab App mẫu:

sudo apt update
sudo apt install openjdk-21-jdk-headless maven
cd ~
git clone https://github.com/AnhTuan2111/devops-self-learning.git
cd devops-self-learning/lab/app-mau
mvn -q package
java -jar target/app-mau.jar

Đợi tới khi thấy dòng [app-mau] PID = … rồi mở cửa sổ Ubuntu thứ hai và gọi thử:

curl http://localhost:8080/
curl -i http://localhost:8080/ghi-chu

Kỳ vọng lệnh đầu trả về tên app và PID, lệnh sau trả 500. Quay lại cửa sổ thứ nhất, tìm trong log dòng có chữ refused và chép nó vào notes.md. Trước khi đọc tiếp, thử tự trả lời: dòng đó nói app đã gọi tới đâu, và vì sao bị từ chối?

Lab 4: Giết app bằng hai cách

Đây là Lab 4 và Lab 5 của Bài 03, lần này không còn chữ "tương đương". App đang chạy ở cửa sổ 1.

# Cửa sổ 2
ps -ef | grep java       # cột thứ hai là PID
kill <PID>               # không ghi số: gửi SIGTERM

# Cửa sổ 1, sau khi app thoát
echo $?

Ghi lại dòng cuối app in ra và con số của echo $?. Rồi chạy lại app ở cửa sổ 1, tìm PID mới, và lần này gõ kill -9 <PID>. Ghi lại hai thứ đó lần nữa và so với Bảng 5.

Còn một cách dừng thứ ba là bấm Ctrl+C ở cửa sổ 1. Theo Bài 03 thì Ctrl+C gửi SIGINT, signal số 2. Thử đoán exit status trước khi bấm, rồi kiểm lại bằng echo $?.

Lab 5: Tự gây lỗi, chạy hai bản cùng lúc

Để app chạy ở cửa sổ 1. Sang cửa sổ 2, vào đúng thư mục lab/app-mau rồi chạy thêm một bản:

cd ~/devops-self-learning/lab/app-mau
java -jar target/app-mau.jar
echo $?

Bản thứ hai phải chết với thông báo ở Hình 7, và exit status của nó khác 0. Giờ tự sửa: tìm process đang giữ port 8080 bằng ps -ef | grep java, kill nó, rồi chạy lại bản thứ hai. Lần này nó phải lên được. Exit status của bản bị kill và của bản chết vì tranh port khác nhau ở đâu, và vì sao?

Bản bị kill để lại 143, vì nó chết do một signal. Bản tranh port thì tự thoát sau khi báo lỗi, nên nó để lại một con số nhỏ khác 0 chứ không phải 128 cộng số signal.

Lab 6: Phép đo đối chứng

Đây là cột phải của Hình 1. Viết dự đoán vào notes.md trước, rồi gõ trong Ubuntu:

time curl -4 -o /dev/null http://127.0.0.1:9999

Dạng kỳ vọng, chưa phải output thật:

curl: (7) Failed to connect to 127.0.0.1 port 9999 after 0 ms: Couldn't connect to server

real    0m0.0…s

Câu chữ sau dấu hai chấm thì mỗi bản curl viết một khác, nên con số mới là thứ cần so. Địa chỉ 127.0.0.1 ở đây là loopback của chính máy ảo.

Ghi con số của bạn cạnh con số 2,155 giây. Lệnh, máy và port đều như nhau, nên chỗ chênh là chênh giữa hai kernel.

Bảng tra triệu chứng

Theo lối của Bài 02: biết ai viết ra thông báo là khoanh được vùng cần nghi.

Bảng 6 Triệu chứng, ai viết ra, lý do, cách sửa.
Thấy gìAi viếtLý doSửa
command not foundbashMáy chưa có chương trình tên đó, hoặc gõ sai tênKiểm tên; sudo apt install
No such file or directoryChương trình, theo lỗi kernel trảSai đường dẫn, sai hoa thường, hoặc đang đứng ở một thư mục khácpwd; thử đường dẫn bắt đầu bằng /
Permission deniedChương trình, theo lỗi kernel trảUser của process không có quyền với filesudo cho đúng lệnh cần root
Port 8080 was already in useSpring Boot, theo lỗi kernel trảĐã có process khác listen port đóps -ef | grep java, rồi kill
500 kèm Connection to localhost:5432 refused trong logApp mẫuKhông có chương trình nào listen port 5432: máy chưa có PostgreSQLChưa sửa trong bài này
Killed, exit status 137bashProcess con bị SIGKILLTìm xem ai đã gửi kill -9
The system cannot accept the time enteredcmd.exeLệnh bash gõ vào cmd.exeChạy trong Ubuntu hoặc Git Bash
0x80370102 khi càiWindowsẢo hoá chưa bật trong BIOS/UEFIBật ảo hoá theo hướng dẫn của hãng máy
Kẹt trong một màn hìnhnanoChương trình đang chiếm terminal, chờ phím của nóCtrl+X với nano, Ctrl+C với lệnh đang chạy

Tự kiểm tra

Bấm vào dòng nào bạn tự giải thích được.

  • Giải thích được WSL2 là một máy ảo có kernel Linux riêng, và vì sao cần hypervisor
  • Phân biệt được terminal, shell và bash
  • Tới một thư mục, xem và sửa một file bằng nano mà không cần chuột
  • Chạy được app mẫu trên Linux và tìm được PID của nó
  • Đọc được 143 và 137 nói app chết vì signal nào
  • Đọc được lỗi Port 8080 was already in use và tự sửa được
  • Nói được app mẫu đang thiếu gì, và vì sao cài tay thứ đó là một chỗ kẹt

Những chỗ hay hiểu sai

Bảng 7 Những cách hiểu nghe hợp lý nhưng sai khi dựng phòng lab.
Dễ nghĩ làThực tếXem lại
WSL2 là trình giả lập LinuxWSL2 chạy kernel Linux thật trong máy ảo. Dịch syscall là cách của WSL 1WSL2
Terminal và shell là mộtTerminal chuyển chữ, shell hiểu chữ. Một terminal chạy được nhiều shellTerminal và shell
Lệnh kill là giết ngayNó gửi một signal. Không ghi số thì là SIGTERM, và app được dọn dẹp rồi mới thoátGiết app bằng hai cách
App khởi động xong là app dùng đượcApp mẫu lên và listen 8080, nhưng đường dẫn cần database vẫn trả 500 vì chưa có PostgreSQLApp chạy, nhưng chưa làm được việc

Kết lại

Câu hỏi của bài là làm sao có một máy Linux thật ngay trên máy mình, để thấy process, signal và lỗi đúng như trên server. Câu trả lời đi qua ba bước. Bước thứ nhất, WSL2 chạy một kernel Linux thật trong một máy ảo nhẹ, và Ubuntu đặt lên đó một bộ chương trình.

Bước thứ hai, bash với vài lệnh là đủ để điều khiển máy đó, tức đi lại, đọc và sửa file, cài phần mềm, tìm và dừng một process.

Bước thứ ba, app mẫu chạy được trên máy đó, và signal giờ là signal thật: kill để lại 143 và app kịp dọn dẹp, còn kill -9 để lại 137 và app không kịp làm gì. Nhưng app chưa làm được việc của nó, vì nó cần PostgreSQL mà máy chưa có.

Câu hỏi còn mở

Để chạy được app, bạn đã phải cài Java bằng tay. Giờ app lại đòi PostgreSQL, tức thêm một lượt cài, tạo user, đặt mật khẩu, tạo database, và sang máy khác thì làm lại tất cả. Từ đó còn lại một câu hỏi:

App mẫu cần PostgreSQL, mà cài tay thì lâu và mỗi máy mỗi khác. Có cách nào chạy PostgreSQL mà không phải cài nó không?

Đọc thêm, không bắt buộc

Phụ lục Lịch sử WSL: vì sao Windows chạy được Linux, từ máy ảo đầy đủ, qua Cygwin và MSYS2 (gốc của Git Bash), tới WSL 1 dịch syscall và WSL2 với kernel thật.

Phụ lục Terminal và shell: bảy bước bash làm với một dòng lệnh, PATH, và vì sao cd nằm sẵn trong bash.

Nguồn đọc thêm

  1. Microsoft Learn, Install WSL. learn.microsoft.com/…/install
  2. Microsoft Learn, Comparing WSL Versions. learn.microsoft.com
  3. Microsoft Learn, Hyper-V architecture (câu "Hyper-V features a Type 1 hypervisor-based architecture"). learn.microsoft.com
  4. Oracle VirtualBox, User Guide, mục Features (VirtualBox là "hosted hypervisor", còn gọi "type 2"). virtualbox.org/manual
  5. Microsoft Learn, Basic commands for WSL. learn.microsoft.com
  6. Microsoft Learn, Set up a WSL development environment. learn.microsoft.com
  7. Microsoft Learn, Troubleshooting Windows Subsystem for Linux. learn.microsoft.com
  8. GNU, Bash Reference Manual, mục Exit Status: lệnh chết vì signal số N thì exit status là 128 cộng N. gnu.org/software/bash/manual
  9. man7.org, signal(7): số hiệu của từng signal.
  10. Spring Boot, Graceful Shutdown: app xử lý nốt request đang dở khi nhận SIGTERM. docs.spring.io

Nguồn ảnh

Sơ đồ lấy từ Wikimedia Commons, theo giấy phép ghi ở dòng của nó, đã chuyển sang đen trắng. Truyện tranh của xkcd và của turnoff.us dùng theo giấy phép phi thương mại ghi ở từng dòng, đã đổi cỡ và chuyển sang đen trắng.

  1. Ảnh 1Hai kiểu hypervisor, Scsami, Wikimedia Commons, CC0.
  2. Ảnh 2Sandwich, Randall Munroe, xkcd.com, CC BY-NC 2.5.
  3. Ảnh 3Signals, Daniel Stori, turnoff.us, CC BY-NC-SA 4.0.
  4. Ảnh 4Terminal password typing, Daniel Stori, turnoff.us, CC BY-NC-SA 4.0.