Computer Systems: A Programmer's Perspective Chương 8 · Exceptional Control Flow

Chương 8 · Luồng điều khiển ngoại lệ Chapter 8 · Exceptional Control Flow

Khi luồng điều khiển bẻ lái: ngoại lệ, tiến trình và tín hiệu Khi chương trình bị cắt ngang: ngắt, tiến trình và tín hiệu When control flow swerves: exceptions, processes, and signals

Từ lúc bật nguồn cho tới khi tắt máy, bộ đếm chương trình (program counter) chạy qua một chuỗi giá trị địa chỉ lệnh; mỗi bước nhảy trơn tru trong chuỗi ấy là luồng điều khiển thông thường. Bryant và O'Hallaron dành trọn chương này cho những lúc luồng ấy bẻ lái đột ngột — cái mà họ gọi là luồng điều khiển ngoại lệ (exceptional control flow, ECF) — xảy ra ở mọi tầng của hệ thống.1

Bình thường máy chạy hết lệnh này tới lệnh kế, êm ru như đọc một trang sách. Nhưng đôi khi có việc gấp chen ngang: bàn phím gõ Ctrl+C, đĩa báo đã xong, hay chương trình chia cho 0. Lúc đó máy phải tạm dừng, lo việc gấp, rồi mới quay lại. Những lúc "chen ngang" đó là trọng tâm của chương. Nhớ nhé: chương này nói về lúc máy bị cắt ngang và cách nó xử lý.

From the time you power on a processor until you shut it off, the program counter runs through a sequence of instruction addresses; each smooth step is ordinary control flow. Bryant and O'Hallaron devote this chapter to the moments that flow swerves abruptly — what they call exceptional control flow (ECF) — which occurs at every level of a system.1

ECF là một sợi chỉ hợp nhất nhiều cơ chế tưởng chừng rời rạc: ngoại lệ phần cứng (hardware exception), chuyển ngữ cảnh (context switch) của hệ điều hành, tín hiệu (signal) ở mức phần mềm, và nhảy phi cục bộ (nonlocal jump) ở mức ngôn ngữ C. Các tác giả nêu rõ lý do người lập trình ứng dụng cần hiểu ECF: nó giúp hiểu các khái niệm hệ thống quan trọng, hiểu cách ứng dụng tương tác với hệ điều hành, viết được những chương trình đồng thời hấp dẫn như shell và server, hiểu cơ chế luồng (flow) của phần mềm, và hiểu cách xử lý tín hiệu.1

Luận đề của chương: ngoài luồng điều khiển trơn tru do rẽ nhánh và gọi hàm tạo ra, hệ thống còn dùng luồng điều khiển ngoại lệ ở mọi tầng — ngoại lệ phần cứng, tiến trình và chuyển ngữ cảnh của nhân, tín hiệu, và nhảy phi cục bộ — làm cơ chế nền tảng để cung cấp tính đồng thời và để ứng dụng tương tác an toàn với hệ điều hành.1

Ý chính: máy không chỉ chạy lệnh theo thứ tự. Nó còn có nhiều cách "nhảy cóc" khi có việc gấp: ngắt từ phần cứng, chia lượt giữa các chương trình, gửi tín hiệu, và nhảy vọt trong mã C. Nhớ nhé: hiểu các cú nhảy cóc này là hiểu cách máy làm nhiều việc cùng lúc.

The chapter's thesis: beyond the smooth control flow produced by branches and calls, a system relies on exceptional control flow at every level — hardware exceptions, the kernel's processes and context switches, signals, and nonlocal jumps — as the basic mechanism for providing concurrency and letting applications interact safely with the operating system.1

Cụm khái niệm 01 Concept cluster 01

Ngoại lệ: phần cứng và nhân hợp tác để bẻ lái luồng điều khiển Ngoại lệ: lúc máy bị cắt ngang và ai lo việc gấp Exceptions: hardware and kernel cooperate to divert control flow

Một ngoại lệ (exception) là sự thay đổi đột ngột của luồng điều khiển để đáp lại một thay đổi nào đó trong trạng thái của bộ xử lý. Thay đổi trạng thái ấy gọi là một sự kiện (event): có thể liên quan trực tiếp tới lệnh đang chạy (lỗi trang bộ nhớ ảo, tràn số học, chia cho 0), hoặc không liên quan (bộ định thời reo, một yêu cầu I/O hoàn tất).1

"Ngoại lệ" là lúc máy đang chạy một lệnh thì có chuyện xảy ra, buộc nó phải dừng lại ngay và nhảy đi lo chuyện đó. Chuyện có thể do chính lệnh đang chạy gây ra (như chia cho 0), hoặc do bên ngoài (như bàn phím, đĩa báo tin). Nhớ nhé: ngoại lệ = máy đang chạy thì bị một sự kiện cắt ngang.

An exception is an abrupt change in the control flow in response to some change in the processor's state. That state change is called an event: it may be directly related to the current instruction (a virtual-memory page fault, an arithmetic overflow, a divide by zero), or unrelated to it (a system timer goes off, an I/O request completes).1

Định nghĩa. Mỗi loại ngoại lệ được gán một số nguyên không âm duy nhất gọi là số hiệu ngoại lệ (exception number). Khi khởi động, hệ điều hành dựng một bảng nhảy gọi là bảng ngoại lệ (exception table) sao cho mục k chứa địa chỉ bộ xử lý ngoại lệ k. Khi sự kiện xảy ra, bộ xử lý dùng số hiệu làm chỉ số vào bảng — địa chỉ nền của bảng nằm trong một thanh ghi riêng gọi là exception table base register — rồi gọi gián tiếp tới bộ xử lý tương ứng, chạy trong chế độ nhân (kernel mode).1

Nói đơn giản. Mỗi loại sự cố có một con số riêng. Máy giữ một "bảng tra địa chỉ": tra số sự cố ra địa chỉ đoạn mã lo sự cố đó, rồi nhảy tới đó chạy. Nhớ nhé: mỗi sự cố có một số, và một bảng tra chỉ chỗ xử lý.

Definition. Each kind of exception is assigned a unique nonnegative integer exception number. At boot time the OS builds a jump table — the exception table — so that entry k holds the address of the handler for exception k. When the event occurs, the processor uses the number as an index into the table (whose base address lives in a special exception table base register) and makes an indirect call to the handler, which runs in kernel mode.1

Lớp | Đồng bộ? | Quay lại
Ngắt | async | lệnh kế tiếp
Bẫy | sync | lệnh kế tiếp
Lỗi | sync | lệnh hiện tại / huỷ
Huỷ | sync | không quay lại

Ngắt → do bên ngoài
Bẫy → cố ý gọi nhân
Lỗi → có thể sửa
Huỷ → chết luôn

Class | Sync? | Returns to
Interrupt | async | next instr.
Trap | sync | next instr.
Fault | sync | current instr. / abort
Abort | sync | never returns

  1. exception handling

    Cơ chế xử lý — nửa phần cứng, nửa phần mềm Cách xử lý — phần cứng với phần mềm cùng lo Exception handling — half hardware, half software

    Ngoại lệ khó hiểu vì nó đòi phần cứng và phần mềm phối hợp chặt chẽ. Giống một lời gọi thủ tục, bộ xử lý đẩy địa chỉ trả về lên ngăn xếp trước khi nhảy tới bộ xử lý; nhưng khác ở chỗ nó còn đẩy thêm trạng thái bộ xử lý (ví dụ trên x86-64 là thanh ghi EFLAGS chứa mã điều kiện), và khi đi từ chương trình người dùng vào nhân, mọi thứ được đẩy lên ngăn xếp của nhân chứ không phải ngăn xếp người dùng.1

    Phần cứng lo phần đầu: phát hiện sự cố và nhảy tới đúng đoạn mã. Phần mềm (bộ xử lý ngoại lệ) lo phần sau: giải quyết sự cố rồi quyết định có quay lại chương trình cũ hay không. Nhớ nhé: phần cứng bắt lấy sự cố, phần mềm giải quyết nó.

    Exceptions are hard because they need close hardware–software cooperation. Like a procedure call, the processor pushes a return address before branching to the handler; unlike one, it also pushes extra processor state (e.g. x86-64 pushes the EFLAGS register with the condition codes), and when crossing from a user program into the kernel, everything is pushed onto the kernel's stack, not the user's.1

    Bạn gặp nó ở đâuWhere you meet it Mỗi khi chương trình gọi một dịch vụ của hệ điều hành, hay khi đĩa báo "đã đọc xong" — phần cứng đang âm thầm bẻ lái qua bảng ngoại lệ. Mỗi lần máy phản ứng với bàn phím, đĩa, hay một lỗi — cơ chế này đang chạy. Every time a program requests an OS service, or a disk signals "read complete" — the hardware is quietly diverting through the exception table.

    Ba kết cục có thể

    Sau khi xử lý xong, bộ xử lý ngoại lệ làm một trong ba việc: trả điều khiển về lệnh hiện tại Icurr, trả về lệnh kế tiếp Inext, hoặc huỷ chương trình bị cắt ngang — tuỳ loại sự kiện gây ra ngoại lệ.1

    Return from interrupt

    Nếu quay lại, bộ xử lý thực thi một lệnh đặc biệt "return from interrupt": pop trạng thái đã lưu trở lại các thanh ghi, khôi phục chế độ người dùng (nếu ngoại lệ đã cắt ngang một chương trình người dùng), rồi trả điều khiển.1

  2. four classes

    Bốn lớp — ngắt, bẫy, lỗi, huỷ Bốn kiểu — ngắt, bẫy, lỗi, huỷ Four classes — interrupts, traps, faults, aborts

    Ngoại lệ chia thành bốn lớp. Ngắt (interrupt) xảy ra không đồng bộ do tín hiệu từ thiết bị I/O bên ngoài, luôn quay về lệnh kế tiếp. Ba lớp còn lại xảy ra đồng bộ do thực thi một lệnh. Bẫy (trap) là ngoại lệ cố ý, dùng để hiện thực lời gọi hệ thống, cũng quay về lệnh kế tiếp. Lỗi (fault) có thể sửa được: nếu sửa xong thì chạy lại lệnh gây lỗi, nếu không thì huỷ. Huỷ (abort) là lỗi phần cứng không khắc phục được và không bao giờ quay lại.1

    Có bốn kiểu cắt ngang. Ngắt: do bên ngoài (bàn phím, đĩa), xong thì chạy tiếp. Bẫy: chương trình cố ý xin nhân làm việc gì đó. Lỗi: có thể sửa rồi làm lại, như thiếu một trang bộ nhớ. Huỷ: hỏng nặng, chết luôn. Nhớ nhé: ngắt–bẫy–lỗi–huỷ, chỉ "huỷ" là không quay lại.

    Exceptions fall into four classes. Interrupts occur asynchronously from external I/O signals and always return to the next instruction. The other three occur synchronously from executing an instruction. Traps are intentional exceptions used to implement system calls, also returning to the next instruction. Faults are potentially recoverable: if repaired, the faulting instruction is re-executed, otherwise the program aborts. Aborts are unrecoverable hardware errors that never return.1

    Bạn gặp nó ở đâuWhere you meet it Lỗi trang (page fault) là ví dụ kinh điển của lớp "lỗi": lệnh truy cập một trang chưa có trong bộ nhớ, bộ xử lý nạp trang đó từ đĩa rồi cho lệnh chạy lại.1 Khi chương trình cần một mẩu dữ liệu chưa nằm trong bộ nhớ, máy lẳng lặng nạp vào rồi chạy lại. A page fault is the classic "fault": an instruction touches a page not yet in memory, the handler loads it from disk, then the instruction re-executes.1

    Bẫy và lời gọi hệ thống

    Ứng dụng cần dịch vụ của nhân — đọc tệp (read), tạo tiến trình (fork), nạp chương trình (execve), kết thúc (exit) — qua lệnh syscall. Từ góc nhìn lập trình viên, lời gọi hệ thống giống hệt một lời gọi hàm, nhưng nó chạy trong chế độ nhân với toàn quyền.1

    Một lưu ý thuật ngữ

    Thuật ngữ khác nhau giữa các hệ thống: nhiều đặc tả ISA tách "interrupt" (không đồng bộ) khỏi "exception" (đồng bộ). Sách dùng "exception" làm thuật ngữ chung, chỉ phân biệt khi cần.1

  3. Linux / x86-64

    Ngoại lệ trên Linux/x86-64 — những con số cụ thể Ngoại lệ trên Linux/x86-64 — số hiệu cụ thể Linux/x86-64 exceptions — the concrete numbers

    Hệ x86-64 có tới 256 loại ngoại lệ. Các số từ 0 đến 31 do kiến trúc sư Intel định nghĩa nên giống nhau trên mọi hệ x86-64; các số từ 32 đến 255 là ngắt và bẫy do hệ điều hành định nghĩa. Vài ví dụ: số 0 là divide error (lỗi chia, lớp fault; Linux báo "Floating exception"), số 13 là general protection fault (Linux báo "Segmentation fault"), số 14 là page fault, số 18 là machine check (lớp abort).13

    Trên máy x86-64 chạy Linux, mỗi loại sự cố có một số. Số 0–31 là của Intel đặt (giống nhau mọi máy), số 32–255 là của hệ điều hành. Ví dụ "Segmentation fault" quen thuộc chính là ngoại lệ số 13. Nhớ nhé: "Segmentation fault" là một ngoại lệ phần cứng có số hiệu hẳn hoi.

    An x86-64 system has up to 256 exception types. Numbers 0–31 are defined by Intel's architects and so are identical on every x86-64 system; numbers 32–255 are OS-defined interrupts and traps. Examples: number 0 is divide error (a fault; Linux reports "Floating exception"), number 13 is the general protection fault (Linux reports "Segmentation fault"), number 14 is the page fault, and number 18 is a machine check (an abort).13

    Bạn gặp nó ở đâuWhere you meet it Lời gọi hệ thống trên x86-64 dùng lệnh syscall: theo quy ước, %rax giữ số hiệu lời gọi, tối đa sáu tham số nằm trong %rdi, %rsi, %rdx, %r10, %r8, %r9; khi trả về, %rax chứa giá trị trả về.19 Khi chương trình xin nhân làm việc gì, nó đặt "số việc" vào một thanh ghi rồi gọi syscall. A system call on x86-64 uses the syscall instruction: by convention %rax holds the call number, up to six arguments go in %rdi, %rsi, %rdx, %r10, %r8, %r9, and on return %rax holds the result.19

    Một vài lời gọi quen thuộc

    Mỗi lời gọi có một số hiệu riêng: read là 0, write là 1, open là 2, fork là 57, execve là 59, _exit là 60, kill là 62. Bảng nhảy của lời gọi hệ thống khác với bảng ngoại lệ.19

    Hàm bọc của thư viện C

    Hiếm khi lập trình viên gọi syscall trực tiếp; thư viện C chuẩn cung cấp các hàm bọc (wrapper) tiện lợi, đóng gói tham số, bẫy vào nhân, rồi trả kết quả về. Sách gọi chung lời gọi hệ thống và hàm bọc là hàm mức hệ thống (system-level function).1

Cụm khái niệm 02 Concept cluster 02

Tiến trình: hai ảo giác, chuyển ngữ cảnh, và xử lý lỗi hệ thống Tiến trình: ảo giác "một mình một máy" và cách chia lượt Processes: two illusions, context switches, and error handling

Định nghĩa kinh điển: một tiến trình (process) là một thể hiện của một chương trình đang thực thi. Mỗi chương trình chạy trong ngữ cảnh (context) của một tiến trình nào đó — gồm mã và dữ liệu trong bộ nhớ, ngăn xếp, nội dung các thanh ghi, bộ đếm chương trình, biến môi trường, và tập các mô tả tệp (file descriptor) đang mở. Tiến trình trao cho chương trình hai trừu tượng then chốt: một luồng điều khiển logic (logical control flow) và một không gian địa chỉ riêng (private address space).1

"Tiến trình" là một chương trình đang chạy. Hệ điều hành cho mỗi tiến trình cảm giác như nó chiếm trọn bộ xử lý và trọn bộ nhớ, dù thực ra máy đang chạy rất nhiều chương trình. Nhớ nhé: tiến trình = chương trình đang chạy, được cho ảo giác "một mình một máy".

The classic definition: a process is an instance of a program in execution. Each program runs in the context of some process — its code and data in memory, its stack, its registers, its program counter, environment variables, and its set of open file descriptors. A process gives the program two key abstractions: an independent logical control flow and a private address space.1

Định nghĩa. Hai luồng điều khiển logic là đồng thời (concurrent) nếu thời gian thực thi của chúng chồng lấn nhau — bất kể chạy trên một hay nhiều lõi. Việc nhiều luồng chạy đan xen gọi là đa nhiệm (multitasking) hay chia thời gian (time slicing). Khi (và chỉ khi) hai luồng chạy trên các lõi khác nhau thì mới gọi là song song (parallel).1

Nói đơn giản. "Đồng thời" là hai việc có khoảng thời gian chồng lên nhau — kể cả khi chỉ có một người làm, luân phiên qua lại. "Song song" mới là thật sự hai người làm cùng lúc. Nhớ nhé: đồng thời chưa chắc song song.

Definition. Two logical flows are concurrent if their executions overlap in time — whether on one core or many. Multiple flows interleaving is multitasking or time slicing. Only when two flows run on different cores are they parallel.1

Ba trạng thái tiến trình:
  Running — chạy / chờ xếp lịch
  Stopped — tạm dừng tới khi SIGCONT
  Terminated — dừng vĩnh viễn
Vùng user bắt đầu ở 0x400000

đang chạy → tạm dừng → chấm dứt
  tín hiệu đẩy qua lại các trạng thái

Three process states:
  Running — on CPU or ready
  Stopped — until a SIGCONT
  Terminated — stopped forever
User region starts at 0x400000

  1. logical flow

    Luồng điều khiển logic và tính đồng thời Luồng logic — mỗi chương trình tưởng mình chiếm CPU Logical control flow and concurrency

    Nếu dùng trình gỡ lỗi chạy từng bước, ta sẽ thấy một dãy giá trị PC chỉ thuộc về chương trình của mình — đó là luồng điều khiển logic. Thực tế một luồng vật lý duy nhất của bộ xử lý bị chia thành nhiều luồng logic, mỗi tiến trình một luồng; chúng thay phiên dùng bộ xử lý: chạy một lúc rồi bị chiếm quyền trước (preempt) để tiến trình khác chạy.1

    Mỗi chương trình thấy một chuỗi lệnh liền mạch của riêng nó, tưởng mình độc chiếm máy. Thật ra máy chia lượt rất nhanh: chạy chương trình này một nhịp, tạm ngừng, chạy chương trình kia một nhịp. Nhớ nhé: máy luân phiên siêu nhanh nên ai cũng tưởng mình đang chạy một mình.

    If you single-step with a debugger, you see a sequence of PC values belonging only to your program — its logical control flow. The single physical flow of the processor is partitioned into many logical flows, one per process; they take turns using the processor: each runs a while, then is preempted so another can run.1

    Bạn gặp nó ở đâuWhere you meet it Mỗi lần bạn mở nhiều ứng dụng cùng lúc trên một máy chỉ có vài lõi — chúng là các luồng logic đồng thời, luân phiên nhau.1 Khi bạn vừa nghe nhạc vừa gõ văn bản trên cùng một máy. Whenever you run many apps at once on a machine with only a few cores — they are concurrent logical flows taking turns.1

    Đồng thời ≠ song song

    Hai luồng là đồng thời nếu thời gian chạy chồng lấn, kể cả trên cùng một lõi. Song song là tập con của đồng thời: chỉ khi chúng chạy trên các lõi (hoặc máy) khác nhau. Khái niệm đồng thời độc lập với số lõi.1

    Nhiều dạng luồng logic

    Bộ xử lý ngoại lệ, tiến trình, bộ xử lý tín hiệu (signal handler), luồng (thread) đều là các luồng logic. Mỗi khoảng thời gian một tiến trình chạy một phần luồng của nó gọi là một lát thời gian (time slice).1

  2. address space & modes

    Không gian địa chỉ riêng, và chế độ người dùng / nhân Bộ nhớ riêng, và hai chế độ: người dùng và nhân Private address space, and user / kernel mode

    Mỗi tiến trình có một không gian địa chỉ riêng: trên máy địa chỉ n bit, đó là tập 2n địa chỉ, và một byte ở địa chỉ nào đó nói chung không thể bị tiến trình khác đọc hay ghi. Để bảo vệ, bộ xử lý có một bit chế độ (mode bit): khi bật, tiến trình chạy trong chế độ nhân (kernel mode) với toàn quyền; khi tắt, nó ở chế độ người dùng (user mode), không được thực thi lệnh đặc quyền hay chạm trực tiếp vào vùng nhân.1

    Mỗi chương trình có một "bản đồ bộ nhớ" riêng, chương trình khác không thọc vào được. Máy cũng có hai chế độ: chế độ thường (bị hạn chế) và chế độ nhân (toàn quyền). Chương trình của bạn chạy ở chế độ thường cho an toàn. Nhớ nhé: bộ nhớ ai nấy giữ; muốn toàn quyền phải nhờ nhân.

    Each process has a private address space: on a machine with n-bit addresses, the set of 2n addresses, and a byte at an address generally cannot be read or written by any other process. To protect this, the processor has a mode bit: when set, the process runs in kernel mode with full power; when clear, it is in user mode, forbidden from privileged instructions or directly touching the kernel area.1

    Bạn gặp nó ở đâuWhere you meet it Đoạn mã của một tiến trình x86-64 Linux luôn bắt đầu ở địa chỉ 0x400000; cách duy nhất để chuyển từ chế độ người dùng sang chế độ nhân là qua một ngoại lệ — ngắt, lỗi, hoặc một bẫy lời gọi hệ thống.1 Khi chương trình cần làm việc "nhạy cảm", nó phải xin phép nhân qua một lời gọi hệ thống. A x86-64 Linux process's code always begins at 0x400000; the only way to go from user to kernel mode is via an exception — an interrupt, a fault, or a trapping system call.1

    Hệ tệp /proc

    Linux cung cấp /proc, một cơ chế khéo léo phơi bày nội dung nhiều cấu trúc dữ liệu của nhân dưới dạng tệp văn bản mà tiến trình chế độ người dùng đọc được — ví dụ /proc/cpuinfo cho biết loại CPU.1

    Bố cục không gian địa chỉ

    Phần thấp dành cho chương trình người dùng (mã, dữ liệu, heap, ngăn xếp); phần cao dành cho nhân — nơi nhân chạy mã khi thực thi lời gọi hệ thống thay cho tiến trình.1

  3. context switch & errno

    Chuyển ngữ cảnh, và xử lý lỗi lời gọi hệ thống Chuyển lượt giữa các tiến trình, và kiểm tra lỗi Context switches, and system call error handling

    Nhân hiện thực đa nhiệm bằng một dạng ECF cấp cao hơn gọi là chuyển ngữ cảnh (context switch): nó (1) lưu ngữ cảnh của tiến trình hiện tại, (2) khôi phục ngữ cảnh của một tiến trình trước đó đã bị chiếm quyền, và (3) trao điều khiển cho tiến trình vừa khôi phục. Quyết định chuyển lượt gọi là xếp lịch (scheduling), do bộ xếp lịch (scheduler) trong nhân lo. Một ngắt định thời (timer interrupt) — thường mỗi 1 ms hoặc 10 ms — có thể kích hoạt việc chuyển lượt.1

    Khi muốn đổi từ chương trình này sang chương trình khác, nhân cất trạng thái của cái cũ, lấy lại trạng thái cái mới, rồi giao quyền. Cứ vài mili-giây lại có một "tiếng chuông" (ngắt định thời) nhắc nhân cân nhắc đổi lượt. Nhớ nhé: đổi lượt = cất trạng thái cũ, khôi phục trạng thái mới.

    The kernel implements multitasking with a higher-level form of ECF called a context switch: it (1) saves the current process's context, (2) restores a previously preempted process's saved context, and (3) passes control to it. Deciding to switch is scheduling, done by the kernel's scheduler. A timer interrupt — typically every 1 ms or 10 ms — can trigger a switch.1

    Bạn gặp nó ở đâuWhere you meet it Khi một lời gọi như read phải chờ đĩa, nhân có thể chuyển ngữ cảnh sang tiến trình khác thay vì ngồi chờ — nhờ vậy CPU không lãng phí hàng chục mili-giây.1 Khi một chương trình đang chờ đĩa, máy tranh thủ chạy chương trình khác. When a call like read must wait for disk, the kernel can switch to another process instead of idling — so the CPU doesn't waste tens of milliseconds.1

    errno và các hàm bọc lỗi

    Khi hàm mức hệ thống Unix gặp lỗi, nó thường trả về −1 và đặt biến toàn cục errno. Lập trình viên nên luôn kiểm tra lỗi; strerror(errno) trả về chuỗi mô tả. Sách dùng các hàm bọc xử lý lỗi (kiểu Stevens) viết hoa chữ cái đầu — ví dụ Fork gọi fork, kiểm lỗi, và thoát nếu có sự cố.1

    Ngữ cảnh gồm những gì

    Ngữ cảnh mà nhân lưu gồm: các thanh ghi đa dụng và dấu phẩy động, bộ đếm chương trình, ngăn xếp người dùng, thanh ghi trạng thái, ngăn xếp nhân, và các cấu trúc như bảng trang, bảng tiến trình, bảng tệp.1

Cụm khái niệm 03 Concept cluster 03

Điều khiển tiến trình, tín hiệu, và nhảy phi cục bộ Tạo tiến trình, gửi tín hiệu, và cú nhảy vọt trong mã C Process control, signals, and nonlocal jumps

Unix cung cấp một loạt lời gọi hệ thống để thao tác tiến trình từ chương trình C. Ba lời gọi cốt lõi: fork tạo một tiến trình con mới, execve nạp và chạy một chương trình mới trong ngữ cảnh tiến trình hiện tại, và waitpid cho tiến trình cha chờ và "thu hoạch" (reap) con đã kết thúc. Trên hai cơ chế này, Linux thêm tín hiệu (signal) — thông điệp nhỏ báo cho tiến trình một sự kiện — và C thêm nhảy phi cục bộ (nonlocal jump) qua setjmp/longjmp.1

Chương trình C có thể "đẻ" ra chương trình con (fork), "thay da đổi thịt" thành chương trình khác (execve), và "chờ con" xong việc (waitpid). Ngoài ra, các chương trình gửi cho nhau những mẩu tin nhỏ gọi là "tín hiệu". Nhớ nhé: fork đẻ con, execve đổi chương trình, waitpid chờ con.

Unix provides many system calls for manipulating processes from C. Three are central: fork creates a new child process, execve loads and runs a new program in the current process's context, and waitpid lets a parent wait for and reap a terminated child. On top of these, Linux adds signals — small messages notifying a process of an event — and C adds nonlocal jumps via setjmp/longjmp.1

Định nghĩa. fork độc đáo ở chỗ gọi một lần nhưng trả về hai lần: trả về 0 cho tiến trình con, trả về PID của con cho tiến trình cha, và −1 nếu lỗi. Con nhận một bản sao giống hệt nhưng tách biệt không gian địa chỉ của cha, cùng các mô tả tệp đang mở. Ngược lại, execve gọi một lần và không bao giờ trả về (trừ khi lỗi).14

Nói đơn giản. Gọi fork một lần mà thấy "hai bản chạy ra": một bản là cha, một bản là con, giống hệt nhau nhưng từ đó sống riêng. Nhớ nhé: fork gọi một lần, trả về hai lần — 0 cho con, PID cho cha.

Definition. fork is unique: it is called once but returns twice — 0 to the child, the child's PID to the parent, and −1 on error. The child gets an identical but separate copy of the parent's address space plus its open file descriptors. By contrast, execve is called once and never returns (unless it fails).15

pid = fork();
  con → pid == 0
  cha → pid == <PID con>
  lỗi → pid == -1

setjmp() → 0 lần đầu, khác 0 khi longjmp

fork → con thấy 0
       → cha thấy số PID
execve → không quay lại

pid = fork();
  child → pid == 0
  parent → pid == <child PID>
  error → pid == -1

setjmp() → 0 first, nonzero on longjmp

  1. fork / execve / waitpid

    fork, execve, waitpid — đẻ, đổi, và thu hoạch con fork, execve, waitpid — đẻ con, đổi chương trình, chờ con fork, execve, waitpid — spawn, replace, and reap

    Mỗi tiến trình có một PID dương duy nhất (lấy qua getpid; PID của cha qua getppid). Khi một tiến trình kết thúc, nhân không xoá nó ngay mà giữ ở trạng thái đã chấm dứt cho tới khi cha thu hoạch (reap) nó bằng waitpid; một tiến trình đã chết nhưng chưa được thu hoạch gọi là zombie. Nếu cha chết trước, tiến trình init (PID 1) nhận nuôi và thu hoạch các con mồ côi.16

    Khi "con" chạy xong, nó chưa biến mất hẳn — nó thành "xác sống" (zombie) chờ cha đến nhận. Cha phải gọi waitpid để dọn. Nếu cha đi mất, có một "ông tổ" tên init (số 1) đứng ra nhận nuôi. Nhớ nhé: cha phải dọn xác con, kẻo zombie ngốn tài nguyên.

    Each process has a unique positive PID (via getpid; the parent's via getppid). When a process terminates, the kernel does not remove it at once but keeps it in a terminated state until the parent reaps it with waitpid; a dead-but-unreaped process is a zombie. If the parent dies first, the init process (PID 1) adopts and reaps the orphans.16

    Bạn gặp nó ở đâuWhere you meet it Shell và máy chủ web dùng rất nhiều cặp fork + execve: fork ra một con, rồi con gọi execve để biến thành chương trình bạn muốn chạy.1 Khi bạn gõ một lệnh trong terminal: shell đẻ một con, rồi con hoá thành chương trình đó. Shells and web servers lean on fork + execve: fork a child, then the child calls execve to become the program you want to run.1

    Kiểm tra trạng thái thoát

    Nếu truyền một con trỏ statusp khác NULL, waitpid mã hoá trạng thái; tệp wait.h cung cấp các macro giải mã như WIFEXITED (thoát bình thường), WEXITSTATUS (mã thoát), WIFSIGNALED (bị tín hiệu giết), WTERMSIG (số tín hiệu đó).16

    Call once, return twice

    Vì con là bản sao tách biệt, mọi thay đổi của cha hay con lên biến đều riêng tư. Trong ví dụ của sách, biến x bằng 1 ở cả hai ngay sau fork; con tăng thành 2, cha giảm thành 0 — hai bản nhớ độc lập.1

  2. signals

    Tín hiệu — thông điệp nhỏ báo một sự kiện Tín hiệu — mẩu tin báo "có chuyện xảy ra" Signals — a small message about an event

    Một tín hiệu (signal) là một thông điệp nhỏ báo cho tiến trình biết một loại sự kiện đã xảy ra. Linux hỗ trợ 30 loại tín hiệu, mỗi loại ứng với một sự kiện hệ thống. Ví dụ: gõ Ctrl+C gửi SIGINT (số 2); chia cho 0 nhận SIGFPE (số 8); truy cập bộ nhớ sai nhận SIGSEGV (số 11); một tiến trình có thể cưỡng bức giết tiến trình khác bằng SIGKILL (số 9); khi con kết thúc hay dừng, nhân gửi SIGCHLD (số 17) cho cha.12

    Tín hiệu là mẩu tin ngắn báo "có chuyện": bạn bấm Ctrl+C là gửi một tín hiệu bảo chương trình dừng; chia cho 0 cũng sinh một tín hiệu. Mỗi loại tín hiệu có một con số và một cái tên. Nhớ nhé: Ctrl+C chính là gửi tín hiệu số 2 (SIGINT).

    A signal is a small message notifying a process that some event occurred. Linux supports 30 signal types, each tied to a system event. Examples: Ctrl+C sends SIGINT (number 2); dividing by zero yields SIGFPE (8); a bad memory access yields SIGSEGV (11); a process can forcibly kill another with SIGKILL (9); when a child stops or terminates, the kernel sends SIGCHLD (17) to the parent.12

    Bạn gặp nó ở đâuWhere you meet it Nhận một tín hiệu xảy ra hai bước: gửi (nhân cập nhật trạng thái tiến trình đích) rồi nhận (tiến trình phản ứng: bỏ qua, kết thúc, hoặc chạy một bộ xử lý tín hiệu).1 Một tín hiệu được gửi đi rồi mới được nhận; lúc nhận, chương trình có thể lờ đi, dừng, hoặc chạy một đoạn mã xử lý. Receiving a signal takes two steps: sending (the kernel updates the target's state) then receiving (the process reacts: ignore, terminate, or run a signal handler).1

    Đang chờ và bị chặn

    Nhân giữ hai véc-tơ bit cho mỗi tiến trình: pending (tín hiệu đã gửi nhưng chưa nhận) và blocked (tín hiệu bị chặn). Với mỗi loại k, tối đa chỉ có một tín hiệu đang chờ: tín hiệu k thứ hai gửi tới khi đã có một cái chờ sẽ bị bỏ, không xếp hàng.1

    SIGKILL và SIGSTOP đặc biệt

    SIGKILL (9) và SIGSTOP (19) không thể bị bắt cũng không thể bị bỏ qua — đó là lý do kill -9 luôn hạ được một tiến trình bướng bỉnh.12

  3. safe handlers

    Bộ xử lý tín hiệu an toàn — cái bẫy đồng thời Viết bộ xử lý tín hiệu an toàn — tránh lỗi khó lường Safe signal handlers — the concurrency trap

    Bộ xử lý tín hiệu chạy đồng thời với chương trình chính và dùng chung biến toàn cục, nên dễ sinh lỗi đồng thời hiểm hóc. Quy tắc vàng: giữ bộ xử lý càng đơn giản càng tốt (ví dụ chỉ đặt một cờ rồi trả về), và chỉ gọi các hàm an toàn với tín hiệu không đồng bộ (async-signal-safe). Nhiều hàm quen thuộc — printf, sprintf, malloc, exit — không an toàn; cách an toàn duy nhất để in từ bộ xử lý là dùng write.18

    Đoạn mã xử lý tín hiệu chạy xen vào giữa chương trình chính, nên nếu nó đụng vào dữ liệu chung thì dễ gây lỗi lúc được lúc không, rất khó tìm. Mẹo: giữ nó thật ngắn, và đừng gọi những hàm "không an toàn" như printf. Nhớ nhé: trong bộ xử lý tín hiệu, giữ ngắn và chỉ gọi hàm an toàn.

    A signal handler runs concurrently with the main program and shares its global variables, so it easily breeds nasty concurrency bugs. The golden rules: keep handlers as simple as possible (e.g. just set a flag and return), and call only async-signal-safe functions. Many familiar functions — printf, sprintf, malloc, exit — are not safe; the only safe way to print from a handler is write.18

    Bạn gặp nó ở đâuWhere you meet it Một bộ xử lý tốt luôn lưu và khôi phục errno (vì các hàm nó gọi có thể ghi đè), để không làm hỏng việc kiểm tra lỗi của chương trình chính bị cắt ngang.1 Khi chương trình thỉnh thoảng lỗi một cách khó hiểu, thủ phạm thường là bộ xử lý tín hiệu đụng nhầm dữ liệu chung. A good handler saves and restores errno (since the functions it calls may overwrite it), so it doesn't corrupt the interrupted main program's error checking.1
  4. nonlocal jumps

    Nhảy phi cục bộ — thoát thẳng khỏi hàm lồng sâu Nhảy phi cục bộ — cú nhảy vọt qua nhiều tầng hàm Nonlocal jumps — escaping straight out of deep nesting

    C cung cấp một dạng ECF ở mức người dùng gọi là nhảy phi cục bộ (nonlocal jump), chuyển điều khiển thẳng từ hàm này sang một hàm đang chạy khác mà không theo trình tự gọi–trả bình thường. setjmp lưu môi trường hiện tại và trả về 0 lần đầu; longjmp khôi phục môi trường đó khiến setjmp "trả về" lần nữa với một giá trị khác 0. Vậy setjmp gọi một lần nhưng trả về nhiều lần; longjmp gọi một lần và không bao giờ trả về.17

    Bình thường hàm gọi hàm theo kiểu "đi rồi về" từng tầng. Nhảy phi cục bộ cho phép từ sâu trong nhiều tầng hàm nhảy vọt một phát về chỗ đã đánh dấu trước đó — tiện khi gặp lỗi ở tầng sâu. Nhớ nhé: setjmp đặt mốc, longjmp nhảy vọt về mốc đó.

    C offers a user-level form of ECF called a nonlocal jump, transferring control directly from one function to another currently executing one, bypassing the normal call-and-return. setjmp saves the current environment and returns 0 the first time; longjmp restores it, making setjmp "return" again with a nonzero value. So setjmp is called once but returns multiple times; longjmp is called once and never returns.17

    Bạn gặp nó ở đâuWhere you meet it Dùng để trả về ngay từ một lời gọi lồng sâu khi phát hiện lỗi, hoặc để nhảy ra khỏi một bộ xử lý tín hiệu — khi đó dùng cặp sigsetjmp/siglongjmp, ví dụ "khởi động lại mềm" khi người dùng gõ Ctrl+C.1 Khi chương trình cần thoát khẩn khỏi nhiều tầng hàm, hoặc tự khởi động lại khi bạn bấm Ctrl+C. Used to return immediately from a deeply nested call on an error, or to jump out of a signal handler — then using sigsetjmp/siglongjmp, e.g. a "soft restart" when the user hits Ctrl+C.1

    Tương tự try/catch

    Cơ chế ngoại lệ của C++ và Java là phiên bản cấp cao, có cấu trúc của setjmp/longjmp: mệnh đề catch trong try giống setjmp, còn throw giống longjmp.1

    8.7 · Công cụ thao tác tiến trình

    Linux có nhiều công cụ giám sát và thao tác tiến trình: strace (in dấu vết mỗi lời gọi hệ thống), ps (liệt kê tiến trình, kể cả zombie), top (mức dùng tài nguyên), pmap (bản đồ bộ nhớ), và /proc (hệ tệp ảo phơi bày cấu trúc của nhân dạng văn bản).1

Ba ngộ nhận thường gặp Three common misconceptions

“Ngoại lệ chỉ là try/catch trong C++/Java” “Exceptions just mean try/catch in C++/Java”

Ngộ nhận: "ngoại lệ" là một tính năng ngôn ngữ cấp cao.

Thực tế: trong sách, ngoại lệ là cơ chế phần cứng/hệ điều hành ở mức thấp (ngắt, bẫy, lỗi, huỷ). try/catch của C++/Java là "ngoại lệ phần mềm" cấp cao, tương tự setjmp/longjmp.1

Myth: an "exception" is a high-level language feature.

Reality: in the book an exception is a low-level hardware/OS mechanism (interrupts, traps, faults, aborts). C++/Java try/catch are high-level "software exceptions", akin to setjmp/longjmp.1

“fork tạo con dùng chung bộ nhớ với cha” “fork makes a child that shares memory with the parent”

Ngộ nhận: sau fork, cha và con sửa chung một vùng nhớ.

Thực tế: con nhận một bản sao giống hệt nhưng tách biệt; mọi thay đổi sau đó là riêng tư. Chúng dùng chung các tệp đang mở, nhưng không dùng chung không gian địa chỉ.14

Myth: after fork, parent and child edit the same memory.

Reality: the child gets an identical but separate copy; later changes are private. They share open files, not the address space.14

“Gọi printf trong bộ xử lý tín hiệu là an toàn” “Calling printf in a signal handler is safe”

Ngộ nhận: bộ xử lý tín hiệu nào cũng in bằng printf được.

Thực tế: printf, malloc, exit không async-signal-safe. Cách in an toàn duy nhất là write; hãy giữ bộ xử lý đơn giản và chỉ gọi hàm an toàn.18

Myth: any signal handler can print with printf.

Reality: printf, malloc, exit are not async-signal-safe. The only safe print is write; keep handlers simple and call only safe functions.18

Tự kiểm Self-check

Năm câu hỏi để biết bạn đã nắm luồng điều khiển ngoại lệ Năm câu hỏi kiểm tra nhanh Five questions to check you grasped exceptional control flow

Trả lời được những câu này nghĩa là bạn đã theo trọn ECF từ tầng phần cứng, qua tiến trình và chuyển ngữ cảnh, tới tín hiệu và nhảy phi cục bộ.

Nếu trả lời được, nghĩa là bạn đã hiểu chương này.

Answering these means you followed ECF from the hardware level, through processes and context switches, to signals and nonlocal jumps.

  1. Kể tên bốn lớp ngoại lệ và cho biết lớp nào xảy ra không đồng bộ. Lớp nào có thể chạy lại lệnh gây ra nó?

    Gợi ý sâu hơn: "Segmentation fault" ứng với ngoại lệ số mấy trên x86-64, và nó thuộc lớp nào?

  2. Một tiến trình trao cho chương trình hai trừu tượng nào? Phân biệt "đồng thời" với "song song".

    Gợi ý sâu hơn: chuyển ngữ cảnh gồm ba bước nào, và điều gì có thể kích hoạt nó?

  3. Vì sao nói fork "gọi một lần, trả về hai lần"? Giá trị trả về cho con và cho cha khác nhau ra sao?

    Gợi ý sâu hơn: zombie là gì, ai thu hoạch con mồ côi, và PID của tiến trình đó là bao nhiêu?

  4. Tín hiệu là gì? Gõ Ctrl+C gửi tín hiệu nào (số mấy), và tín hiệu nào không thể bị bắt hay bỏ qua?

    Gợi ý sâu hơn: véc-tơ pending và blocked khác nhau thế nào; điều gì xảy ra với tín hiệu k thứ hai khi đã có một cái đang chờ?

  5. Vì sao gọi printf trong một bộ xử lý tín hiệu là nguy hiểm? Nên dùng hàm nào để in an toàn?

    Gợi ý sâu hơn: nhảy phi cục bộ khác lời gọi–trả bình thường ở điểm nào, và nó giống try/catch ra sao?

  1. "Ngoại lệ" là gì? Kể hai thứ có thể cắt ngang một chương trình đang chạy.

  2. "Tiến trình" là gì? Vì sao nhiều chương trình trông như chạy cùng lúc?

  3. Lệnh nào tạo ra một chương trình con? Sau đó con và cha có dùng chung bộ nhớ không?

  4. Bấm Ctrl+C là bạn đang làm gì với chương trình?

  5. Vì sao đoạn mã xử lý tín hiệu nên thật ngắn và cẩn thận?

  1. Name the four classes of exceptions and say which occur asynchronously. Which can re-execute the instruction that caused it?

  2. What two abstractions does a process give a program? Distinguish "concurrent" from "parallel".

  3. Why is fork "called once, returns twice"? How do the return values to child and parent differ?

  4. What is a signal? Which signal (number) does Ctrl+C send, and which signal cannot be caught or ignored?

  5. Why is calling printf in a signal handler dangerous? Which function should you use to print safely?

Khép lại chương Closing the chapter

Luồng điều khiển không phải lúc nào cũng trơn tru: ở mọi tầng, hệ thống bẻ lái luồng điều khiển để đáp lại sự kiện. Ngoại lệ phần cứng dựng nên tiến trình, tiến trình sinh ra chuyển ngữ cảnh và tín hiệu, còn C cho ta cú nhảy phi cục bộ — tất cả là một cơ chế chung mang tên ECF, nền tảng của tính đồng thời.1

Máy không chỉ chạy lệnh theo thứ tự; nó còn biết tạm dừng để lo việc gấp rồi quay lại. Chính những cú "bẻ lái" đó — ngắt, tiến trình, tín hiệu, cú nhảy vọt — giúp một cái máy làm được nhiều việc một lúc. Nhớ nhé: hiểu các cú bẻ lái này là hiểu cách máy làm nhiều việc cùng lúc.

Control flow is not always smooth: at every level, a system swerves its control flow in response to events. Hardware exceptions build processes, processes give us context switches and signals, and C gives us the nonlocal jump — all one mechanism named ECF, the foundation of concurrency.1