Bài 04 · Phụ lục · đọc thêm, không bắt buộc

Terminal và shell: một dòng lệnh đi qua những đâu

Bài 04 chỉ dạy vừa đủ shell để gõ được lệnh. Phụ lục này đi chậm hơn: vì sao có hai chương trình terminal và shell, bash làm gì từ lúc bạn bấm Enter, và nó tìm chương trình ở đâu.

Phụ lục · Terminal và shell Bài giảng 04

Tab Shell của Bài 04 nói terminal chỉ chuyển chữ còn shell mới hiểu chữ, rồi đi thẳng vào các lệnh. Phụ lục này quay lại chỗ đó. Nó không thêm lệnh nào mới, chỉ giải thích những gì xảy ra phía sau mấy lệnh bạn đã gõ, và muốn hiểu thì chỉ cần ba điều về process đã gặp ở Bài 03:

  • process cha sinh ra process con;
  • mỗi process có một thư mục làm việc;
  • mỗi process giữ một bản sao biến môi trường riêng.

Vì sao lại là hai chương trình

Gõ lệnh thì chỉ thấy một cửa sổ, sao lại phải tách thành terminal và shell? Vì ngày xưa chúng nằm ở hai nơi khác nhau. Máy tính thì đặt trong phòng máy, còn thứ để trên bàn người dùng chỉ là một thiết bị nhập và xuất, nối dây về máy tính đó. Thiết bị ấy gọi là terminal, và Ảnh 1 là hai thế hệ của nó.

Máy Teletype Model 33 ASR đặt trên chân đế kim loại, gồm bàn phím và bộ phận in giấy.
teletype 1968 · in ra giấy
Terminal DEC VT100 với màn hình chữ phía trên và bàn phím rời phía dưới.
vt100 màn hình chữ
Ảnh 1 Hai thế hệ terminal. Trái: máy Teletype Model 33 ASR sản xuất năm 1968, bàn phím cộng máy in, mọi output in ra giấy. Phải: terminal DEC VT100, thay giấy bằng màn hình chữ. Cả hai chỉ là thiết bị nhập và xuất, nối dây tới một máy tính ở chỗ khác, và shell chạy trên máy tính đó. Linux vẫn gọi thiết bị terminal là tty, viết tắt của teletypewriter.

Terminal thời đó không chạy chương trình nào cả. Nó gửi phím bạn gõ theo dây về máy tính, và in ra những chữ máy tính gửi lại. Chương trình đọc các phím đó và hiểu chúng là lệnh thì chạy trên máy tính, và đó là shell.

Terminal ngày nay là chương trình giả lập những thiết bị đó (terminal emulator), ví dụ Windows Terminal hay cửa sổ của Git Bash. Cách chia việc thì vẫn như cũ: terminal chuyển chữ, còn shell hiểu chữ.

Một terminal, nhiều shell

Chia việc như vậy thì được gì? Được chỗ này: terminal không cần biết bên trong nó đang chạy shell nào. Vì vậy cùng một cửa sổ Windows Terminal mở được cmd.exe, PowerShell hay bash, và đổi shell thì ngôn ngữ của lệnh đổi theo.

Mỗi shell hiểu dòng lệnh theo cách của nó, nên cùng một dòng mà gõ vào shell khác thì có thể thành việc khác hẳn. Ví dụ chữ time trong bash là từ khoá đo thời gian, còn trong cmd.exe nó lại là lệnh xem và đặt giờ hệ thống. Đây là output thật khi phép đo của Bài 01 bị gõ nhầm vào cmd.exe:

> time curl -4 -o /dev/null http://127.0.0.1:9999
The system cannot accept the time entered.
Enter the new time: 2
A required privilege is not held by the client.
Hình 1 Đúng lệnh, sai shell (output thật trên máy mình, chạy trong cmd.exe). cmd.exe hiểu phần đứng sau chữ time là một giờ mới. Nó từ chối, hỏi lại, rồi báo thiếu quyền, vì đặt giờ hệ thống cần quyền quản trị.

Bản thân lệnh không sai, chỉ là một câu đúng cú pháp bash đã bị đọc theo cú pháp của cmd.exe. Vì vậy khi một lệnh chép từ tài liệu về mà báo lỗi lạ, việc đầu tiên nên xem là mình đang gõ vào shell nào.

Bạn gõ ls -l /etc rồi bấm Enter, và một danh sách hiện ra. Giữa hai việc đó bash đã làm những gì?

Bảy bước của một dòng lệnh

Bash làm bảy bước, vẽ ở Hình 2, và lặp lại đúng bảy bước đó cho mọi dòng bạn gõ.

  bash (PID 812)                          kernel
  ─────────────────────────────────────   ──────────────────────────────
  1  in dấu nhắc        ten@may:~$
  2  đọc dòng           ls -l /etc
  3  tách chữ           ls   -l   /etc
  4  tìm "ls"           không nằm sẵn trong bash
                        tìm theo PATH: /usr/bin/ls
  5  fork, rồi exec  ──────────────────►  tạo process con PID 913
                                          chạy /usr/bin/ls
  6  đợi             ◄──────────────────  PID 913 thoát, exit status 0
  7  ghi 0 vào $?, quay lại bước 1
Hình 2 Vòng đời của một dòng lệnh. Bước 5 là fork và exec của Bài 03. Số PID chỉ để minh hoạ.

Bước 3 cắt dòng lệnh thành các chữ theo dấu cách. Muốn giữ nguyên một cụm có dấu cách thì bọc nó trong dấu nháy, như grep -i "killed process" ở Bài 03. Nếu không có dấu nháy thì grep nhận hai chữ rời nhau, và nó hiểu chữ thứ hai là tên một file cần tìm trong đó.

Bước 5 là chỗ nối với Bài 03. Bash không tự chạy ls bên trong mình. Nó nhờ kernel nhân bản chính nó thành một process con, rồi process con đó thay chương trình của mình bằng ls. Vì thế lệnh nào bạn gõ cũng là con của bash, và mang theo bản sao biến môi trường của bash.

Bước 6 và 7 là chỗ sinh ra exit status mà Bài 04 dùng để đọc 143 và 137. Bash đứng đợi process con, và khi process con thoát thì kernel báo lại cho bash con số nó để lại. Nếu process con chết vì một signal thì kernel báo số của signal đó, và bash tự đổi thành 128 cộng số signal.

Cả bức tranh trong một hình vẽ

Ảnh 2 vẽ những thứ vừa kể thành một toà nhà: terminal, process, và kernel là bên trông coi tất cả.

Tranh vẽ một toà nhà cắt lớp, tiêu đề Inside the Linux Kernel. Các chim cánh cụt đeo số trên ngực đi lại trên sàn. Tầng trên là các màn hình ghi tty, chú thích terminals and terminal processes. Giữa nhà có một chồng ống ghi pipes. Một con đứng ở cửa số 80, chú thích httpd process; một con ở cửa số 22, chú thích ssh daemon. Một nhóm ngồi quanh bàn, chú thích process table. Dưới sàn là các tủ hồ sơ, chú thích filesystem. Còn có cron, wine process và mấy con chó watchdog.
Ảnh 2 Inside the Linux Kernel của Daniel Stori, turnoff.us, năm 2016 (bấm vào ảnh để xem cỡ lớn). Mỗi chim cánh cụt là một process, số trên ngực là PID. Các terminal (tty) ở tầng trên là nơi process đọc phím gõ và in chữ ra. Chồng ống ở giữa nhà là đường nối output của lệnh này sang lệnh khác, tức dấu | mà Bài 04 dùng trong ps -ef | grep java. Con đứng ở cửa số 80 là một web server đang listen port 80. Cái bàn có nhãn process table là một trò chơi chữ: table vừa là bàn, vừa là bảng process mà kernel giữ. Tranh đặt mọi process vào trong kernel cho dễ nhìn. Trên máy thật thì process chạy ở user space, còn kernel là bên giữ bảng, cửa và đường ống. Các nhãn còn lại (cron, watchdog, filesystem, cửa 22) nằm ngoài phụ lục này.

Ở bước 4 của Hình 2, bash phải trả lời một câu: chữ ls ứng với file nào trên đĩa?

PATH là một danh sách thư mục

Bash tìm câu trả lời trong biến môi trường PATH. PATH là một danh sách thư mục, ngăn nhau bằng dấu hai chấm. Bash tìm lần lượt từng thư mục, và dùng file tên ls ở thư mục đầu tiên có nó. Vì PATH là biến môi trường nên đúng như Bài 03 đã nói, mỗi process mang một bản sao, và process con nhận bản sao từ cha.

$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:…:/mnt/c/WINDOWS/system32:…
$ which ls
/usr/bin/ls
$ type cd
cd is a shell builtin
$ khongcolenhnay
khongcolenhnay: command not found
Hình 3 Bốn câu hỏi về lệnh (output kỳ vọng, PATH đã rút gọn). Lệnh which cho biết bash sẽ dùng file nào, còn type cho biết một cái tên là loại lệnh gì. Trong WSL, cuối PATH có thêm thư mục của Windows, nên từ Ubuntu vẫn gọi được chương trình Windows.

Đọc lại lỗi command not found

Vậy lỗi command not found nghĩa là gì? Nghĩa là không thư mục nào trong PATH có file mang tên đó. Bài 04 nói gọn là "máy chưa có chương trình tên đó", còn nói cho đủ thì có ba lý do: gõ sai tên, chưa cài, hoặc đã cài nhưng ở một chỗ nằm ngoài PATH.

Lý do thứ ba là lý do hay làm mất thời gian nhất, vì chương trình có thật trên đĩa mà gõ tên vẫn không chạy. Gặp chuyện đó thì có hai cách. Một là gõ thẳng đường dẫn đầy đủ của file, ví dụ /opt/cong-cu/bin/abc, vì đã ghi đường dẫn thì bash không cần tìm trong PATH nữa. Hai là thêm thư mục chứa nó vào PATH.

Cũng vì bash chỉ tìm trong PATH nên chương trình nằm ngay trong thư mục bạn đang đứng vẫn không chạy được bằng tên trần. Phải gõ ./abc, trong đó ./ nghĩa là "ở chính thư mục này". Đó là lý do các hướng dẫn trên Linux hay có dạng ./mvnw hay ./gradlew.

Ở Hình 3, type cd trả lời rằng cd là một shell builtin. Nó khác gì ls, và vì sao phải khác?

Lệnh nằm sẵn trong bash

Một builtin là lệnh nằm sẵn bên trong bash chứ không phải một file trên đĩa. Gặp builtin thì bash tự làm lấy, không fork và không exec, nên Hình 2 dừng ở bước 4.

Vì sao cd phải là builtin? Lý do đi thẳng từ Bài 03: thư mục làm việc là thuộc tính của từng process. Giả sử cd là một chương trình riêng. Khi đó bash sẽ chạy nó thành một process con, process con đó đổi thư mục làm việc của chính nó rồi thoát, còn bash thì vẫn đứng nguyên ở thư mục cũ. Vậy là gõ cd xong chẳng có gì đổi.

  nếu cd là một chương trình riêng            cd là builtin

  bash          đứng ở /home/ten             bash          đứng ở /home/ten
    │ fork, exec                               │ tự đổi
    ▼                                          ▼
  cd /etc       đổi sang /etc, rồi thoát     bash          đứng ở /etc
    │
    ▼
  bash          vẫn đứng ở /home/ten
Hình 4 Process con không đổi được thư mục làm việc của process cha, nên việc đổi thư mục phải do chính bash làm.

Vì vậy cd phải do chính bash làm, để đổi thư mục của chính bash. Lệnh export ở Bài 03 là builtin vì cùng lý do: nó đổi biến môi trường của chính bash, để các process con sinh ra sau đó nhận được bản sao mới.

Thư mục làm việc và đường dẫn tương đối

Thư mục làm việc của bash thì dùng vào việc gì? Mọi đường dẫn không bắt đầu bằng / đều được nối vào sau nó. Đó chính là dòng "đường dẫn tương đối tính từ đây" trong Hình 6 của Bài 03. Lấy lệnh cat ghi-chu.txt làm ví dụ. Đứng ở /home/ten thì nó mở /home/ten/ghi-chu.txt. Sau khi cd /etc thì vẫn lệnh đó nhưng nhận về No such file or directory, vì giờ nó đi tìm /etc/ghi-chu.txt.

Lab 5 của Bài 04 có một chỗ dựa vào đúng điều này. Lệnh java -jar target/app-mau.jar dùng một đường dẫn tương đối, nên phải cd vào thư mục lab/app-mau rồi mới gõ được. Gõ ở chỗ khác thì Java báo không tìm thấy file jar.

Gom lại

Bảng 1 Những gì đứng sau một dòng lệnh, và lỗi mỗi thứ sinh ra.
ThứLàm gìLỗi hay gặp
TerminalChuyển phím gõ vào, hiện chữ raKhông sinh lỗi về lệnh
ShellHiểu dòng lệnh theo cú pháp của nóLệnh bash gõ vào cmd.exe thành việc khác
PATHNơi bash tìm chương trình theo têncommand not found
fork và execChạy chương trình thành một process conProcess con không đổi được gì của bash
Thư mục làm việcGốc để tính đường dẫn tương đốiNo such file or directory
Exit statusCon số process con để lại cho bashKhác 0 là thất bại, trên 128 là chết vì signal

Nối về bài chính

Bài 04 cần bạn gõ được chừng chục lệnh để chạy và giết app mẫu, nên nó chỉ nói mỗi lệnh làm gì. Phụ lục này cho thấy phía sau mỗi lệnh đó là cùng một vòng bảy bước, và ba lỗi hay gặp nhất khi gõ lệnh đều đọc được bằng vòng đó: command not found là bước tìm theo PATH không ra, No such file or directory là đường dẫn tính từ một thư mục làm việc khác, còn một exit status lạ là con số process con để lại ở bước cuối.

Từ đây, quay lại tab Shell của Bài 04, hoặc đi thẳng tới Lab.

Nguồn đọc thêm

  1. GNU, Bash Reference Manual, các mục Command Search and Execution, Bourne Shell Builtins và Exit Status. gnu.org/software/bash/manual
  2. man7.org, fork(2) và execve(2): hai bước sinh ra một process mới.
  3. man7.org, chdir(2): syscall đổi thư mục làm việc của process gọi nó.
  4. Microsoft Learn, lệnh time của cmd.exe. learn.microsoft.com

Nguồn ảnh

Ảnh chụp lấy từ Wikimedia Commons, theo giấy phép ghi ở từng dòng, đã chuyển sang đen trắng, tách khỏi nền cũ và đặt vào khung vuông. Truyện tranh của turnoff.us dùng theo giấy phép phi thương mại ghi ở dòng của nó, đã chuyển sang đen trắng.

  1. Ảnh 1Teletype Model 33 ASR (1968), Dennis van Zuijlekom, Wikimedia Commons, CC BY-SA 2.0.
  2. Ảnh 1DEC VT100 terminal, Jason Scott, Wikimedia Commons, CC BY 2.0.
  3. Ảnh 2Inside the Linux Kernel, Daniel Stori, turnoff.us, CC BY-NC-SA 4.0.