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
-
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ếpInext, hoặc huỷ chương trình bị cắt ngang — tuỳ loại sự kiện gây ra ngoại lệ.1Return 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
-
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.1Bẫ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ệnhsyscall. 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.1Mộ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
-
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ệnhsyscall: theo quy ước,%raxgiữ 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ề,%raxchứ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ọisyscall. A system call on x86-64 uses thesyscallinstruction: by convention%raxholds the call number, up to six arguments go in%rdi, %rsi, %rdx, %r10, %r8, %r9, and on return%raxholds the result.19Một vài lời gọi quen thuộc
Mỗi lời gọi có một số hiệu riêng:
readlà 0,writelà 1,openlà 2,forklà 57,execvelà 59,_exitlà 60,killlà 62. Bảng nhảy của lời gọi hệ thống khác với bảng ngoại lệ.19Hàm bọc của thư viện C
Hiếm khi lập trình viên gọi
syscalltrự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
-
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
-
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 at0x400000; the only way to go from user to kernel mode is via an exception — an interrupt, a fault, or a trapping system call.1Hệ 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/cpuinfocho biết loại CPU.1Bố 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
-
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ưreadphả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 likereadmust wait for disk, the kernel can switch to another process instead of idling — so the CPU doesn't waste tens of milliseconds.1errno 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ụForkgọifork, kiểm lỗi, và thoát nếu có sự cố.1Ngữ 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
-
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 quagetppid). 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ằngwaitpid; 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ìnhinit(PID 1) nhận nuôi và thu hoạch các con mồ côi.16Khi "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êninit(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 viagetppid). When a process terminates, the kernel does not remove it at once but keeps it in a terminated state until the parent reaps it withwaitpid; a dead-but-unreaped process is a zombie. If the parent dies first, theinitprocess (PID 1) adopts and reaps the orphans.16Bạn gặp nó ở đâuWhere you meet it Shell và máy chủ web dùng rất nhiều cặpfork+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 onfork+execve: fork a child, then the child calls execve to become the program you want to run.1Kiểm tra trạng thái thoát
Nếu truyền một con trỏ
statuspkhác NULL,waitpidmã hoá trạng thái; tệpwait.hcung 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 đó).16Call 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
xbằng 1 ở cả hai ngay saufork; con tăng thành 2, cha giảm thành 0 — hai bản nhớ độc lập.1 -
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ậnSIGFPE(số 8); truy cập bộ nhớ sai nhậnSIGSEGV(số 11); một tiến trình có thể cưỡng bức giết tiến trình khác bằngSIGKILL(số 9); khi con kết thúc hay dừng, nhân gửiSIGCHLD(số 17) cho cha.12Tí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 yieldsSIGFPE(8); a bad memory access yieldsSIGSEGV(11); a process can forcibly kill another withSIGKILL(9); when a child stops or terminates, the kernel sendsSIGCHLD(17) to the parent.12Bạ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
-
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ùngwrite.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 iswrite.18Bạn gặp nó ở đâuWhere you meet it Một bộ xử lý tốt luôn lưu và khôi phụcerrno(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 restoreserrno(since the functions it calls may overwrite it), so it doesn't corrupt the interrupted main program's error checking.1 -
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.
setjmplưu môi trường hiện tại và trả về 0 lần đầu;longjmpkhôi phục môi trường đó khiếnsetjmp"trả về" lần nữa với một giá trị khác 0. Vậysetjmpgọi một lần nhưng trả về nhiều lần;longjmpgọi một lần và không bao giờ trả về.17Bì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.
setjmpsaves the current environment and returns 0 the first time;longjmprestores it, makingsetjmp"return" again with a nonzero value. Sosetjmpis called once but returns multiple times;longjmpis called once and never returns.17Bạ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ặpsigsetjmp/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 usingsigsetjmp/siglongjmp, e.g. a "soft restart" when the user hits Ctrl+C.1Tươ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 đềcatchtrongtrygiốngsetjmp, cònthrowgiốnglongjmp.18.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.
-
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?
-
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ó?
-
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?
-
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ờ?
-
Vì sao gọi
printftrong 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/catchra sao?
"Ngoại lệ" là gì? Kể hai thứ có thể cắt ngang một chương trình đang chạy.
"Tiến trình" là gì? Vì sao nhiều chương trình trông như chạy cùng lúc?
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?
Bấm Ctrl+C là bạn đang làm gì với chương trình?
Vì sao đoạn mã xử lý tín hiệu nên thật ngắn và cẩn thận?
Name the four classes of exceptions and say which occur asynchronously. Which can re-execute the instruction that caused it?
What two abstractions does a process give a program? Distinguish "concurrent" from "parallel".
Why is
fork"called once, returns twice"? How do the return values to child and parent differ?What is a signal? Which signal (number) does Ctrl+C send, and which signal cannot be caught or ignored?
Why is calling
printfin 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