Chương 3 · Khái niệm lập trình hệ thống Chapter 3 · System Programming Concepts
Những quy ước chung mà mọi chương về sau đều dựa vào Luật chơi chung trước khi vào việc thật The shared conventions every later chapter rests on
Kerrisk mở chương bằng những chủ đề là điều kiện tiên quyết cho lập trình hệ thống: một system call (lời gọi hệ thống) hoạt động ra sao, một library function (hàm thư viện) khác nó thế nào, thư viện C chuẩn và hiện thực GNU của nó (glibc) là gì, và — quan trọng nhất — vì sao ta phải luôn kiểm tra trạng thái trả về của mỗi lời gọi.1 Chương khép lại bằng chuyện viết chương trình khả chuyển: feature test macro và các kiểu dữ liệu hệ thống của SUSv3.1
Trước khi dạy bạn mở tệp hay tạo tiến trình, tác giả dừng lại dựng "luật chơi": khi chương trình nhờ nhân Linux làm việc thì chuyện gì xảy ra bên dưới, làm sao biết lời nhờ đó thành công hay thất bại, và làm sao viết mã chạy được trên nhiều hệ UNIX khác nhau. Nhớ nhé: chương này là bộ quy ước, học một lần dùng cho cả cuốn.
Kerrisk opens with topics that are prerequisites for system programming: how a system call works, how a library function differs from it, what the standard C library and its GNU implementation (glibc) are, and — above all — why we must always check the return status of every call.1 It closes on writing portable programs: feature test macros and the SUSv3 standard system data types.1
Điểm đáng chú ý về phương pháp: chương 3 không giới thiệu một API cụ thể nào để "dùng ngay", mà thiết lập từ vựng và thói quen được lặp lại trong hàng trăm trang về sau. Khi các chương sau viết "lời gọi system call xyz()", họ ngầm hiểu đó là gọi hàm bọc (wrapper) trong thư viện C — một quy ước được định nghĩa chính tại đây.1 Tương tự, mọi chương trình ví dụ trong sách đều gọi các hàm bắt lỗi dùng chung (errMsg(), errExit()…) và biên dịch với cùng một tập feature test macro, nên hiểu chương này là điều kiện để đọc trôi chảy phần mã nguồn của những chương sau.1
Luận đề của chương: ranh giới giữa chương trình người dùng và nhân Linux được bắc qua bởi system call — một cổng vào có kiểm soát, đi kèm chi phí và quy ước báo lỗi riêng; hiểu cơ chế đó, phân biệt nó với hàm thư viện, luôn kiểm tra errno, và tôn trọng các ràng buộc khả chuyển là nền móng cho mọi dòng mã hệ thống đúng đắn.1
Ý chính: chương trình của bạn không tự chạm vào nhân máy; nó phải "gõ cửa" qua system call, và mỗi lần gõ cửa đều có thể thất bại. Biết cách gõ, cách kiểm tra kết quả, và cách viết cho nhiều máy là đủ nền để đi tiếp. Nhớ nhé: gõ cửa nhân thì luôn phải xem cửa có mở không.
The chapter's thesis: the boundary between a user program and the Linux kernel is crossed by a system call — a controlled entry point with its own cost and error-reporting convention; understanding that mechanism, distinguishing it from a library function, always checking errno, and respecting portability constraints is the foundation of every correct line of systems code.1
Cụm khái niệm 01 Concept cluster 01
System call là một cổng vào nhân có kiểm soát — và việc đi qua nó tốn công System call là cửa để nhờ nhân, và mỗi lần qua cửa đều mất chút công A system call is a controlled entry point into the kernel — and crossing it costs work
Một system call là một điểm vào có kiểm soát (controlled entry point) để một tiến trình yêu cầu nhân thực hiện một hành động nào đó thay cho nó — tạo tiến trình mới, thực hiện I/O, tạo pipe, v.v.1 Nhìn từ phía lập trình, gọi một system call trông giống gọi một hàm C, nhưng bên trong có nhiều bước xảy ra: chuyển bộ xử lý từ user mode sang kernel mode để truy cập được bộ nhớ nhân được bảo vệ.12
Nhân Linux giống một quầy dịch vụ có bảo vệ. Chương trình không được tự vào trong, mà phải đưa yêu cầu qua một ô cửa duy nhất — đó là system call. Nhờ cửa này mà chương trình "xin" nhân mở tệp, tạo tiến trình, gửi dữ liệu… Nhìn thì giống gọi một hàm bình thường, nhưng thật ra máy phải chuyển sang chế độ đặc quyền rồi mới làm. Nhớ nhé: muốn nhờ nhân thì phải đi qua đúng một cái cửa, và cửa đó có bảo vệ.
A system call is a controlled entry point into the kernel through which a process requests that the kernel perform some action on its behalf — creating a new process, performing I/O, creating a pipe, and so on.1 From a programming point of view, invoking a system call looks like calling a C function, but behind the scenes many steps occur: the processor switches from user mode to kernel mode so the CPU can reach protected kernel memory.12
Định nghĩa. Tập hợp các system call là cố định, và mỗi lời gọi được nhận diện bằng một số hiệu duy nhất (số này thường không hiện ra với chương trình, vốn gọi theo tên). Trên Linux/x86-32, execve() là system call số 11 (__NR_execve); nhân dùng số đó để tra bảng sys_call_table và chạy đúng routine phục vụ.12
Nói đơn giản. Mỗi loại yêu cầu có một con số riêng, như số thứ tự quầy. Bạn gọi theo tên, còn bên trong nhân tra theo số. Nhớ nhé: mỗi system call có một con số định danh.
Definition. The set of system calls is fixed, and each call is identified by a unique number (normally invisible to programs, which identify calls by name). On Linux/x86-32, execve() is system call number 11 (__NR_execve); the kernel uses that number to index sys_call_table and run the right service routine.12
user: wrapper() --> set %eax = __NR_xxx
--[trap: int 0x80]--> kernel mode
--> system_call(): check nr
--> sys_xxx() service routine
--> return value --> errno if < 0
gọi hàm bọc --> đặt số hiệu
--> bẫy vào nhân
--> nhân làm việc
--> trả kết quả (hoặc lỗi)
user: wrapper() --> set %eax = __NR_xxx
--[trap: int 0x80]--> kernel mode
--> system_call(): check nr
--> sys_xxx() service routine
--> return value --> errno if < 0
-
wrapper + trap
Hàm bọc và lệnh bẫy — cách thực sự bước vào nhân Hàm trung gian và cú "bẫy" vào nhân The wrapper and the trap — how you really enter the kernel
Chương trình không bẫy thẳng vào nhân; nó gọi một hàm bọc (wrapper) trong thư viện C. Hàm bọc chép các đối số vào đúng thanh ghi, đặt số hiệu lời gọi vào thanh ghi
%eax, rồi phát một lệnh bẫy (trap) — trên x86-32 cổ điển làint 0x80— khiến bộ xử lý chuyển sang kernel mode và nhảy vào routinesystem_call().1Chương trình của bạn không tự "nhảy" vào nhân. Nó nhờ một hàm trung gian trong thư viện C: hàm này xếp sẵn các thông tin cần thiết rồi ra một tín hiệu đặc biệt để nhân tiếp quản. Nhớ nhé: bạn gọi hàm bọc, hàm bọc mới gõ cửa nhân hộ bạn.
A program never traps straight into the kernel; it calls a wrapper function in the C library. The wrapper copies the arguments into the right registers, places the call number into
%eax, then issues a trap instruction — classicallyint 0x80on x86-32 — which switches the processor to kernel mode and jumps into thesystem_call()routine.1Bạn gặp nó ở đâuWhere you meet it Khi dùngstraceđể xem một chương trình gọi những system call nào — chính là nhìn thấy các cú bẫy này diễn ra theo thời gian thực.1 Khi bạn chạystraceđể "soi" xem chương trình đang nhờ nhân làm gì. When you usestraceto watch which system calls a program makes — you are literally seeing these traps happen in real time.1Chi tiết của tác giả
Các kiến trúc x86-32 mới hơn hiện thực lệnh
sysenter, cho một lối vào kernel mode nhanh hơnint 0x80. Việc dùngsysenterđược hỗ trợ từ nhân 2.6 và từ glibc 2.3.2 trở đi.1Vì sao điều này quan trọng
Routine
system_call()lưu thanh ghi lên stack của nhân, kiểm tra tính hợp lệ của số hiệu, kiểm tra địa chỉ các đối số có trỏ vào vùng hợp lệ trong bộ nhớ người dùng, rồi mới gọi routine phục vụ — đó là nguồn gốc của phần lớn chi phí một system call.1 -
kernel mode
Đổi chế độ — vì sao không thể bỏ qua cái cửa Đổi sang chế độ đặc quyền — vì sao phải vậy The mode switch — why the gate can't be skipped
Lý do cốt lõi của mọi nghi thức này là bảo vệ: một system call chuyển bộ xử lý từ user mode sang kernel mode để CPU được phép truy cập bộ nhớ nhân vốn bị che. Mỗi lời gọi có thể mang một tập đối số chỉ định thông tin cần chuyển từ không gian người dùng (vùng địa chỉ ảo của tiến trình) vào không gian nhân, và ngược lại.1
Nhân giữ những vùng nhớ và tài nguyên quan trọng mà chương trình thường không được đụng vào. Chỉ khi đã "đổi thẻ" sang chế độ đặc quyền, máy mới cho phép chạm tới. Vì thế không có đường tắt: muốn nhờ nhân thì phải qua cửa và đổi chế độ. Nhớ nhé: đổi chế độ chính là lớp bảo vệ, không phải thủ tục thừa.
The core reason for all this ceremony is protection: a system call changes the processor from user mode to kernel mode so the CPU may access the shielded kernel memory. Each call may carry a set of arguments specifying information to transfer from user space (the process's virtual address space) into kernel space, and vice versa.1
Bạn gặp nó ở đâuWhere you meet it Mỗi lần một lời gọi báo lỗiEFAULT(bad address) vì con trỏ bạn truyền trỏ ra ngoài vùng nhớ hợp lệ — đó là bước kiểm tra địa chỉ của nhân từ chối. Khi chương trình báo lỗi địa chỉ xấu vì bạn đưa cho nhân một con trỏ sai. Every time a call reportsEFAULT(bad address) because a pointer you passed points outside valid memory — that is the kernel's address check saying no. -
syscall overhead
Chi phí có thật — một con số để nhớ Qua cửa thì mất công — một con số minh hoạ The overhead is real — a number to remember
Dù mỗi bước nhỏ, cộng lại một system call có chi phí đáng kể so với một lời gọi hàm thường. Tác giả đo
getppid()— lời gọi chỉ trả về PID của tiến trình cha: trên một hệ Linux 2.6.25/x86-32, 10 triệu lần gọi mất khoảng 2,2 giây (~0,3 micro-giây mỗi lần). Cũng trên hệ đó, 10 triệu lần gọi một hàm C chỉ trả về một số nguyên chỉ mất 0,11 giây — khoảng một phần hai mươi.19Nhờ nhân làm việc luôn chậm hơn tự làm trong chương trình, vì phải qua cửa và đổi chế độ. Tác giả đo thử: gọi một việc nhỏ xíu của nhân mười triệu lần mất hơn hai giây, còn gọi một hàm thường mười triệu lần nhanh gấp khoảng hai mươi lần. Nhớ nhé: đừng gọi nhân nhiều lần vô ích — mỗi lần qua cửa đều mất công.
Though each step is small, together a system call has appreciable overhead compared with an ordinary function call. The author measured
getppid()— a call that merely returns the parent's PID: on a Linux 2.6.25/x86-32 system, 10 million calls took about 2.2 seconds (~0.3 microseconds each). On the same system, 10 million calls to a C function that just returns an integer took 0.11 seconds — about one-twentieth.19Bạn gặp nó ở đâuWhere you meet it Đây là lý do sâu xa của việc đệm vào/ra (buffering): gom nhiều thao tác nhỏ thành ít lời gọi lớn để trả ít "phí qua cửa" hơn — chủ đề trọng tâm của Chương 13. Đây là lý do người ta "gom hàng": làm một lần nhiều việc thay vì gọi nhân liên tục. This is the deep reason for I/O buffering: batching many small operations into fewer large calls to pay the gate toll less often — the core theme of Chapter 13.
Cụm khái niệm 02 Concept cluster 02
Hàm thư viện khác system call, và glibc là lớp đứng giữa Hàm thư viện không phải system call — glibc đứng ở giữa Library functions are not system calls, and glibc is the layer in between
Một library function chỉ đơn giản là một trong vô số hàm tạo nên thư viện C chuẩn.1 Nhiều hàm thư viện không gọi system call nào cả (ví dụ các hàm thao tác chuỗi). Số khác lại được xây trên system call: fopen() dùng system call open() để thật sự mở tệp, còn printf() thêm định dạng và đệm dữ liệu lên trên write(); malloc() và free() làm công việc sổ sách giúp cấp phát bộ nhớ dễ hơn nhiều so với system call brk() bên dưới.1
Hàm thư viện là hàm có sẵn trong thư viện C. Có hàm chẳng cần nhờ nhân gì cả (như đếm độ dài một chuỗi). Có hàm thì bên trong vẫn phải gọi nhân, nhưng được bọc lại cho dễ dùng: printf() lo việc định dạng rồi mới nhờ nhân ghi; fopen() gọi open() hộ bạn. Nhớ nhé: hàm thư viện là lớp tiện lợi; có cái nhờ nhân, có cái không.
A library function is simply one of the multitude of functions that make up the standard C library.1 Many library functions make no system calls at all (for example, the string-manipulation functions). Others are layered on top of system calls: fopen() uses the open() system call to actually open a file, while printf() adds formatting and data buffering on top of write(); malloc() and free() do bookkeeping that makes allocating memory far easier than the underlying brk() system call.1
Định nghĩa. Có nhiều hiện thực của thư viện C trên các hệ UNIX; hiện thực phổ biến nhất trên Linux là GNU C library, tức glibc.17 Vì hầu hết ứng dụng trên Linux đều liên kết với glibc, cuốn sách giới hạn thảo luận ở thư viện này.1
Nói đơn giản. "Thư viện C" có nhiều phiên bản; trên Linux cái hay dùng nhất tên là glibc. Nhớ nhé: trên Linux, nói "thư viện C" gần như là nói glibc.
Definition. There are several implementations of the C library on UNIX systems; the most common on Linux is the GNU C library, glibc.17 Because nearly every Linux application links against glibc, the book confines its discussion to it.1
fopen() --> open() [system call]
printf() --> write() [+ định dạng, đệm]
malloc() --> brk() [+ sổ sách]
strlen() --> (không gọi nhân)
printf --> write (có thêm định dạng)
fopen --> open
malloc --> brk
strlen --> không cần nhân
fopen() --> open() [system call]
printf() --> write() [+ formatting, buffering]
malloc() --> brk() [+ bookkeeping]
strlen() --> (no kernel call)
-
function vs syscall
Giao diện thân thiện hơn — vì sao có lớp thư viện Vì sao cần lớp thư viện cho dễ dùng A friendlier interface — why the library layer exists
Hàm thư viện thường được thiết kế để cho một giao diện thân thiện với người gọi hơn là system call bên dưới.
write()chỉ xuất một khối byte thô;printf()thêm cho bạn định dạng (%d,%s…) và đệm để giảm số lần thật sự gọi nhân. Đó là lý do một hàm tiện dụng có thể "giấu" một system call bên trong — hoặc chẳng giấu cái nào.1System call thường "thô": làm đúng một việc cơ bản. Hàm thư viện khoác thêm lớp tiện lợi lên trên — định dạng chữ, gom dữ liệu — để bạn viết ít và đúng hơn. Nhớ nhé: thư viện C là lớp áo mềm bọc quanh những lời gọi nhân cứng.
Library functions are often designed to offer a more caller-friendly interface than the underlying system call.
write()just outputs a raw block of bytes;printf()adds formatting (%d,%s…) and buffering that reduces how often the kernel is actually called. That is why a convenience function may "hide" a system call inside — or hide none at all.1Bạn gặp nó ở đâuWhere you meet it Khi đầu raprintf()của bạn "biến mất" lúc chương trình đổ vỡ — vì nó còn nằm trong vùng đệm của thư viện, chưa kịp tớiwrite(). Chương 13 đào sâu điều này. Khi dòng chữ bạn in ra chưa hiện thì chương trình đã tắt — vì nó còn kẹt trong vùng đệm. When yourprintf()output "vanishes" on a crash — because it is still in the library's buffer, not yet handed towrite(). Chapter 13 digs into this. -
glibc version
Biết mình đang chạy glibc nào Làm sao biết phiên bản glibc Knowing which glibc you are running
Đôi khi ta cần biết phiên bản glibc trên máy. Từ shell, có thể chạy chính tệp thư viện chia sẻ
/lib/libc.so.6như một chương trình — nó in ra số phiên bản. Trong mã, từ phiên bản 2.0 trở đi glibc định nghĩa hai hằng__GLIBC__và__GLIBC_MINOR__kiểm tra được lúc biên dịch; còn để biết phiên bản lúc chạy, gọignu_get_libc_version(), trả về một chuỗi như "2.12".17Muốn biết máy đang dùng glibc bản mấy? Gõ chạy thẳng tệp thư viện là nó khoe số phiên bản. Trong chương trình thì có hàm hỏi trực tiếp phiên bản đang chạy. Nhớ nhé: có cả cách hỏi lúc biên dịch lẫn lúc chạy.
Sometimes we need the glibc version on a system. From the shell, we can run the shared library file
/lib/libc.so.6as a program — it prints its version number. In code, from version 2.0 onward glibc defines two constants,__GLIBC__and__GLIBC_MINOR__, testable at compile time; to learn the version at run time, callgnu_get_libc_version(), which returns a string such as "2.12".17Bạn gặp nó ở đâuWhere you meet it Khi một hàm hay một hằng có mặt trên máy này mà thiếu trên máy khác — kiểm tra phiên bản glibc là bước chẩn đoán đầu tiên. Khi mã chạy máy này mà lỗi ở máy khác — hãy soi xem glibc có khác bản không. When a function or constant is present on one machine but missing on another — checking the glibc version is the first diagnostic step.Một lưu ý về nguồn
Sách (2010) nói glibc do Roland McGrath khởi xướng, về sau do Ulrich Drepper duy trì.1 Đây là thông tin theo thời điểm xuất bản; ngày nay glibc được một cộng đồng bảo trì dưới sự điều phối của một ủy ban (steering committee) chứ không một cá nhân — một ví dụ cho thấy chi tiết "ai bảo trì" dễ cũ, trong khi cơ chế thì bền.7
Một cách khác
Cũng có thể lấy phiên bản qua hàm
confstr()với biến cấu hình riêng của glibc_CS_GNU_LIBC_VERSION, trả về chuỗi như "glibc 2.12".1 -
the error twins
Dù là hàm hay system call — đều phải kiểm tra kết quả Hàm hay nhờ nhân, đều phải xem có thành công không Function or system call — both must be checked
Dù một hàm bạn gọi là system call hay hàm thư viện, hầu như lời gọi nào cũng trả về một giá trị trạng thái cho biết thành công hay thất bại — và giá trị đó luôn cần được kiểm tra. Bỏ kiểm tra để "tiết kiệm gõ phím" là một sự tiết kiệm sai lầm: rất nhiều giờ gỡ lỗi bị phí vì không kiểm tra một lời gọi "không thể nào hỏng".1
Dù gọi hàm nào, bạn cũng nên hỏi: "nó có làm được không?" Nhiều lỗi khó tìm chỉ vì người viết tin rằng lời gọi chắc chắn thành công. Nhớ nhé: lười kiểm tra kết quả là tự chuốc giờ gỡ lỗi về sau.
Whether a function you call is a system call or a library function, almost every call returns a status value indicating success or failure — and it should always be checked. Skipping checks to "save typing" is a false economy: many hours of debugging are wasted because a call that "couldn't possibly fail" was not checked.1
Bạn gặp nó ở đâuWhere you meet it Dẫn thẳng sang phần sau: cơ chế chuẩn để báo và đọc lỗi trên UNIX chính là biếnerrno. Phần tiếp theo chỉ cho bạn cách đọc lỗi: biếnerrno. This leads straight into the next part: the standard UNIX mechanism for reporting and reading errors is theerrnovariable.
Cụm khái niệm 03 Concept cluster 03
Lỗi về qua errno — và có đúng một cách đọc nó cho an toàn Lỗi báo qua errno — và cách đọc đúng Errors surface through errno — and there is exactly one safe way to read it
Khi một system call thất bại, nó đặt biến nguyên toàn cục errno thành một giá trị dương định danh lỗi cụ thể. Khai báo của errno cùng một tập hằng tên lỗi nằm trong <errno.h>; mọi tên hằng này đều bắt đầu bằng chữ E (như EINTR, EACCES). Thông thường, thất bại được báo bằng giá trị trả về −1.134
Khi nhờ nhân mà hỏng, chương trình thường nhận lại số −1, và một biến chung tên errno được đặt thành một con số cho biết hỏng vì lý do gì. Mỗi lý do có một cái tên bắt đầu bằng chữ E. Nhớ nhé: thấy −1 thì nhìn ngay sang errno để biết vì sao.
When a system call fails, it sets the global integer errno to a positive value identifying the specific error. The declaration of errno and a set of error-name constants live in <errno.h>; every such name begins with the letter E (such as EINTR, EACCES). Usually, failure is signalled by a return value of −1.134
Quy tắc. Lời gọi thành công không bao giờ đặt lại errno về 0, nên biến này có thể còn mang giá trị cũ từ một lỗi trước. Vì vậy: luôn kiểm tra giá trị trả về trước (có báo lỗi không), rồi mới xem errno để biết nguyên nhân.14
Quy tắc. Thành công thì errno không tự về 0. Nên đừng nhìn errno trước; hãy xem giá trị trả về báo hỏng chưa, rồi mới đọc errno. Nhớ nhé: xem kết quả trước, đọc errno sau.
The rule. A successful call never resets errno to 0, so it may hold a stale value from an earlier error. Therefore: always check the return value first (did it signal an error), and only then read errno for the cause.14
fd = open(path, flags, mode);
if (fd == -1) {
/* lỗi: nay mới đọc errno */
perror("open");
}
fd = open(...);
if (fd == -1) {
perror("open");
}
fd = open(path, flags, mode);
if (fd == -1) {
/* error: now read errno */
perror("open");
}
-
never fails
Vài lời gọi không bao giờ hỏng Có vài lời gọi chẳng bao giờ hỏng A few calls never fail
Có một số ít system call không bao giờ thất bại. Ví dụ
getpid()luôn trả về PID của tiến trình thành công, và_exit()luôn kết thúc được tiến trình. Với những lời gọi như vậy, không cần kiểm tra giá trị trả về.1Một vài việc nhân làm thì không thể hỏng — như hỏi "số định danh của tôi là bao nhiêu" hay "hãy kết thúc tôi". Với mấy việc đó thì khỏi cần kiểm tra. Nhớ nhé: chỉ một số rất ít lời gọi mới được miễn kiểm tra.
A few system calls never fail. For example,
getpid()always successfully returns the process's PID, and_exit()always terminates the process. For such calls there is no need to check the return value.1Bạn gặp nó ở đâuWhere you meet it Khi đọc mã người khác thấygetpid()không được bọc kiểm tra lỗi — đó là đúng, không phải cẩu thả. Khi thấy ai đó không kiểm tragetpid()— yên tâm, vậy là đúng. When you read code wheregetpid()is not wrapped in an error check — that is correct, not careless. -
the -1 trap
Khi −1 lại là kết quả hợp lệ Khi −1 không hẳn là lỗi When −1 is a legitimate result
Một số ít lời gọi có thể hợp lệ trả về −1 khi thành công — ví dụ
getpriority(). Với chúng, cách duy nhất để phân biệt là: đặterrnovề 0 trước khi gọi, rồi sau đó kiểm tra. Nếu lời gọi trả về −1 vàerrnokhác 0 thì mới thực sự có lỗi.14Có vài lời gọi mà −1 cũng là một đáp án đúng, không phải dấu hiệu hỏng. Mẹo: xoá errno về 0 trước khi gọi; sau đó nếu vừa thấy −1 vừa thấy errno khác 0 thì mới là lỗi thật. Nhớ nhé: với mấy lời gọi đặc biệt, xoá errno trước rồi mới kết luận.
A few calls can legitimately return −1 on success — for example
getpriority(). For these, the only way to tell is: seterrnoto 0 before the call, then check afterward. Only if the call returns −1 anderrnois nonzero has an error actually occurred.14Bạn gặp nó ở đâuWhere you meet it Bất cứ khi nào tài liệu man ghi rõ một hàm "có thể trả về −1 khi thành công" — đó là tín hiệu phải xoáerrnotrước. Khi trang hướng dẫn nói hàm có thể trả −1 lúc thành công — nhớ xoá errno trước. Whenever the man page notes a function "may return −1 on success" — that is the signal to clearerrnofirst. -
perror / strerror
Dịch errno thành câu đọc được Biến con số errno thành chữ dễ hiểu Turning errno into readable words
Sau một lời gọi hỏng, cách thông dụng là in một thông báo dựa trên
errno. Hàmperror()in ra chuỗi msg bạn đưa, kèm theo câu mô tả ứng với giá trị hiện tại củaerrno. Hàmstrerror()trả về chuỗi mô tả lỗi ứng với số lỗi trong đối số errnum của nó.15Con số errno khó nhớ, nên có hàm dịch nó ra câu chữ:
perror()in luôn ra màn hình kèm lời nhắc của bạn;strerror()trả về câu mô tả để bạn tự dùng. Nhớ nhé: đừng in số errno trần trụi — hãy để perror/strerror dịch hộ.After a failed call, the common course is to print a message based on
errno. Theperror()function prints the msg string you pass, followed by a message corresponding to the current value oferrno. Thestrerror()function returns the error-description string corresponding to the error number given in its errnum argument.15Bạn gặp nó ở đâuWhere you meet it Mọi chương trình ví dụ trong sách đều dùng các hàm bọc riêng (errMsg(),errExit()) xây trênstrerror()để in cả tên hằng lẫn mô tả lỗi.1 Các ví dụ trong sách đều gói sẵn mấy hàm báo lỗi tiện dụng dựa trên strerror. Every example program in the book uses its own wrappers (errMsg(),errExit()) built onstrerror()to print both the error name and description.1Một chi tiết tinh tế
Chuỗi do
strerror()trả về có thể được cấp phát tĩnh, nghĩa là một lời gọistrerror()sau có thể ghi đè lên nó; cảperror()vàstrerror()đều phụ thuộc locale nên thông báo có thể hiển thị theo ngôn ngữ địa phương.15Hàm thư viện báo lỗi khác nhau
Không phải hàm thư viện nào cũng dùng
errno: một số trả về −1 và đặterrnonhư system call; một số trả giá trị khác −1 (ví dụNULL) nhưng vẫn đặterrno; một số khác không dùngerrnochút nào — với nhóm cuối, dùngerrno/perror()để chẩn đoán là sai.1
Cụm khái niệm 04 Concept cluster 04
Viết cho nhiều máy: feature test macro và kiểu dữ liệu hệ thống Viết sao cho chạy được trên nhiều hệ UNIX Writing for many machines: feature test macros and system data types
Phần cuối chương xoay quanh tính khả chuyển (portability). Đôi khi ta muốn các header chỉ phơi bày đúng những định nghĩa tuân theo một chuẩn nhất định; để làm vậy ta định nghĩa một hoặc nhiều feature test macro trước khi include bất kỳ header nào, hoặc truyền qua tùy chọn -D của trình biên dịch.16 Song song, SUSv3 định nghĩa một tập kiểu dữ liệu hệ thống chuẩn để ta không phải dựa vào kích thước cụ thể của int hay long trên từng hệ.1
Mỗi hệ UNIX một khác chút ít, nên muốn mã chạy được ở nhiều nơi, ta phải "khai báo luật chơi": nói cho header biết mình muốn theo chuẩn nào (feature test macro), và dùng các tên kiểu dữ liệu chung thay vì tự đoán kích thước con số. Nhớ nhé: khai báo chuẩn và dùng kiểu chung thì mã dễ mang đi.
The chapter's final theme is portability. Sometimes we want headers to expose only the definitions conforming to a particular standard; to do that we define one or more feature test macros before including any header, or pass them via the compiler's -D option.16 In parallel, SUSv3 defines a set of standard system data types so we need not depend on the specific size of int or long on a given system.1
Định nghĩa. Để theo SUSv3 (POSIX.1-2001), các ứng dụng tuân thủ định nghĩa _POSIX_C_SOURCE với giá trị 200112 và _XOPEN_SOURCE với giá trị 600. Với SUSv4 (POSIX.1-2008) là 200809 và 700.16 Sách cho biết mọi ví dụ của nó biên dịch được với cc -std=c99 -D_XOPEN_SOURCE=600.1
Nói đơn giản. Mỗi chuẩn có một "mật khẩu" số. Khai đúng mật khẩu thì header mở đúng bộ hàm theo chuẩn đó. Nhớ nhé: feature test macro là cách nói "tôi muốn theo chuẩn này".
Definition. To target SUSv3 (POSIX.1-2001), conforming applications define _POSIX_C_SOURCE as 200112 and _XOPEN_SOURCE as 600. For SUSv4 (POSIX.1-2008) they are 200809 and 700.16 The book notes all its examples compile with cc -std=c99 -D_XOPEN_SOURCE=600.1
/* đặt TRƯỚC mọi #include */
#define _XOPEN_SOURCE 600
#include <unistd.h>
...
pid_t mypid; /* kiểu chuẩn */
printf("%ld\n", (long) mypid);
#define _XOPEN_SOURCE 600
#include <unistd.h>
pid_t mypid;
printf("%ld\n", (long) mypid);
/* put BEFORE any #include */
#define _XOPEN_SOURCE 600
#include <unistd.h>
...
pid_t mypid; /* standard type */
printf("%ld\n", (long) mypid);
-
feature test macros
Feature test macro — bạn chọn chuẩn, header lộ API tương ứng Macro chọn chuẩn — khai trước, header mở đúng bộ hàm Feature test macros — you pick a standard, the header exposes its API
Các macro như
_POSIX_C_SOURCE,_XOPEN_SOURCElà chuẩn-hóa và khả chuyển tới mọi hệ hỗ trợ chúng; ngoài ra glibc còn có các macro riêng như_BSD_SOURCE,_SVID_SOURCE, và_GNU_SOURCE(phơi bày tất cả định nghĩa của các macro trên cộng thêm các mở rộng GNU).16 Điểm mấu chốt: macro phải được định nghĩa trước khi include header thì mới có tác dụng.1Có mấy "mật khẩu" chuẩn dùng được ở mọi nơi, lại có mấy cái riêng của glibc mở thêm đồ của GNU. Quan trọng nhất: phải khai mật khẩu trước khi gọi header, khai sau là vô ích. Nhớ nhé: feature test macro phải đứng trước mọi #include.
Macros such as
_POSIX_C_SOURCEand_XOPEN_SOURCEare standardized and portable to every system that supports them; glibc also has its own macros such as_BSD_SOURCE,_SVID_SOURCE, and_GNU_SOURCE(which exposes all of the preceding definitions plus various GNU extensions).16 The key point: a macro must be defined before including the header to have any effect.1Bạn gặp nó ở đâuWhere you meet it Khi một hàm bạn chắc chắn có thật lại bị trình biên dịch báo "implicit declaration" — rất thường là vì thiếu một feature test macro đúng. Khi biên dịch báo "không khai báo" một hàm có thật — hay là do quên khai macro chuẩn. When a function you know exists draws an "implicit declaration" warning — very often it is a missing feature test macro. -
system data types
Kiểu dữ liệu hệ thống — vì sao dùng pid_t chứ không dùng int Dùng tên kiểu chung thay vì tự đoán kích thước System data types — why pid_t, not int
Các thông tin như process ID, user ID, hay file offset được biểu diễn bằng các kiểu chuẩn chứ không bằng
int/longtrực tiếp, vì kích thước các kiểu cơ bản thay đổi giữa các hệ. Các kiểu này được định nghĩa bằngtypedef, tên thường kết thúc bằng_tvà phần lớn khai báo trong<sys/types.h>. Trên Linux/x86-32,pid_tchẳng hạn làtypedef int pid_t;.18Cùng một thứ (ví dụ "số định danh tiến trình") có thể to nhỏ khác nhau tùy máy. Nên thay vì viết
int, ta dùng tên chung nhưpid_tđể máy nào cũng hiểu đúng. Mấy tên này hay kết thúc bằng "_t". Nhớ nhé: dùng tên kiểu có "_t" để khỏi lệ thuộc vào máy cụ thể.Information such as a process ID, user ID, or file offset is represented with standard types rather than raw
int/long, because the sizes of fundamental types vary across systems. These types are defined withtypedef, their names usually end in_t, and most are declared in<sys/types.h>. On Linux/x86-32,pid_t, for instance, istypedef int pid_t;.18Bạn gặp nó ở đâuWhere you meet it Khi in một giá trị kiểu hệ thống bằngprintf(): cách an toàn là ép vềlongvà dùng%ld— ví dụprintf("%ld\n", (long) mypid);— để tránh lệ thuộc biểu diễn.1 Khi muốn in các con số kiểu hệ thống: ép vềlongrồi dùng%ldcho chắc. When printing a system-type value withprintf(): the safe way is to cast tolongand use%ld— e.g.printf("%ld\n", (long) mypid);— to avoid a representation dependency.1Kiểu cũng đổi theo thời gian
Ngay trên cùng một hệ, kiểu dùng cho một thông tin có thể đổi giữa các bản. Ví dụ user và group ID: trên Linux 2.2 trở về trước biểu diễn bằng 16 bit, còn từ Linux 2.4 trở đi là 32 bit — một lý do nữa để không "đóng đinh" kích thước.1
Ngoại lệ đáng nhớ
Với
off_t(kích thước/độ dời tệp), vì nó có thể làlong longtrong một số môi trường biên dịch, sách ép giá trị vềlong longvà dùng%lldthay vì%ld.1 -
conventions in code
Những quy ước làm cho mã ví dụ ngắn gọn Mấy quy ước giúp ví dụ gọn lại The conventions that keep the example code short
Để các chương trình minh họa gọn và nhất quán, sách dùng một header chung
tlpi_hdr.h(gom các header hay dùng, định nghĩa kiểuBoolean, các macromin()/max()), một bộ hàm bắt lỗi dùng chung, và các hàm phân tích đối số dòng lệnh số (getInt(),getLong()). Mọi chương trình nhiều đối số còn cung cấp sẵn tùy chọn--helpin cú pháp dùng.1Để khỏi viết lại hoài, sách gom sẵn một "hộp đồ nghề" dùng chung: một header tổng, mấy hàm báo lỗi, và hàm đọc số từ dòng lệnh. Nhờ vậy ví dụ nào cũng ngắn và giống nhau. Nhớ nhé: thấy tlpi_hdr.h hay errExit() trong các chương sau là biết chúng đến từ đây.
To keep the illustrative programs short and consistent, the book uses a common header
tlpi_hdr.h(gathering frequently used headers, defining aBooleantype andmin()/max()macros), a shared set of error-handling functions, and numeric command-line parsing functions (getInt(),getLong()). Every program with a nontrivial argument syntax also offers a--helpoption that prints its usage.1Bạn gặp nó ở đâuWhere you meet it Mỗi lần một chương sau mở đầu bằng#include "tlpi_hdr.h"và gọierrExit("...")— những quy ước đó chính là từ chương 3 này. Mỗi khi chương sau dùng tlpi_hdr.h và errExit — đó là "hộp đồ nghề" của chương 3. Every time a later chapter opens with#include "tlpi_hdr.h"and callserrExit("...")— those conventions come from this Chapter 3.
Ba ngộ nhận thường gặp Three common misconceptions
“System call cũng chỉ là một lời gọi hàm bình thường” “A system call is just an ordinary function call”
Ngộ nhận: gọi read() giống hệt gọi một hàm C của bạn.
Thực tế: nó đi qua hàm bọc, một lệnh bẫy, và một lần chuyển sang kernel mode — nên có chi phí thật, như con số đo getppid() cho thấy.1
Myth: calling read() is just like calling one of your C functions.
Reality: it goes through a wrapper, a trap, and a switch to kernel mode — so it has real cost, as the getppid() measurement shows.1
“Thành công thì errno được đặt về 0” “A successful call resets errno to 0”
Ngộ nhận: cứ nhìn errno là biết có lỗi hay không.
Thực tế: lời gọi thành công không đặt lại errno; phải kiểm tra giá trị trả về trước, rồi mới đọc errno.14
Myth: just look at errno to know whether there was an error.
Reality: a successful call does not reset errno; check the return value first, then read errno.14
“Dùng int cho PID, uid là đủ” “Using int for a PID or uid is fine”
Ngộ nhận: kiểu cơ bản như int giống nhau trên mọi máy.
Thực tế: kích thước đổi theo hệ và theo bản (uid từ 16 lên 32 bit giữa Linux 2.2 và 2.4); nên dùng kiểu chuẩn pid_t, uid_t và in bằng (long) + %ld.1
Myth: a fundamental type like int is the same on every machine.
Reality: sizes vary across systems and releases (uid went 16→32 bit between Linux 2.2 and 2.4); use the standard pid_t, uid_t types and print via (long) + %ld.1
Tự kiểm Self-check
Năm câu hỏi để biết bạn đã nắm các quy ước nền Năm câu hỏi kiểm tra nhanh Five questions to check you grasped the conventions
Trả lời được những câu này nghĩa là bạn đã nắm bộ quy ước mà mọi chương sau của sách đều dựa vào.
Nếu trả lời được, nghĩa là bạn đã hiểu chương này.
Answering these means you have the conventions that every later chapter of the book rests on.
-
Vì sao một system call "trông giống" một lời gọi hàm nhưng lại tốn kém hơn? Kể các bước chính từ hàm bọc tới routine phục vụ.
Gợi ý sâu hơn: con số
getppid()trong sách cho thấy chi phí đó lớn cỡ nào so với một hàm C thường? -
Hàm thư viện khác system call ở điểm nào? Cho một ví dụ hàm thư viện có gọi system call và một ví dụ không gọi.
Gợi ý sâu hơn: vì sao
printf()lại được xây trênwrite()thay vì gọi thẳng? -
Trình tự đúng để kiểm tra lỗi của một system call là gì, và vì sao không được nhìn
errnotrước?Gợi ý sâu hơn: với một lời gọi có thể trả về −1 khi thành công (như
getpriority()) thì phải làm thêm gì? -
Feature test macro để làm gì, và vì sao nó phải được định nghĩa trước mọi
#include? Cặp giá trị nào ứng với SUSv3?Gợi ý sâu hơn: lệnh biên dịch mà sách dùng cho mọi ví dụ là gì?
-
Vì sao nên dùng
pid_tthay vìint, và cách an toàn để in một giá trị kiểu hệ thống bằngprintf()là gì?Gợi ý sâu hơn: trường hợp
off_tđược xử lý khác ra sao?
Vì sao nhờ nhân làm việc lại chậm hơn tự làm trong chương trình?
Hàm thư viện và system call khác nhau chỗ nào? Cho một ví dụ mỗi loại.
Khi một lời gọi hỏng, bạn nhìn vào đâu để biết nó hỏng, rồi nhìn đâu để biết vì sao?
Muốn mã chạy được trên nhiều hệ UNIX, ta phải khai báo gì ở đầu chương trình?
Vì sao nên dùng tên kiểu như
pid_tthay vìint?
Why does a system call "look like" a function call yet cost more? Name the main steps from wrapper to service routine.
How does a library function differ from a system call? Give one example that calls a system call and one that does not.
What is the correct order for checking a system call's error, and why must you not read
errnofirst?What are feature test macros for, and why must one be defined before any
#include? Which values target SUSv3?Why use
pid_trather thanint, and what is the safe way to print a system-type value withprintf()?
Khép lại chương Closing the chapter
Lập trình hệ thống bắt đầu từ một ranh giới: chương trình của bạn gõ cửa nhân qua system call, và mỗi lần gõ cửa đều có thể thất bại — nên luôn kiểm tra giá trị trả về rồi mới đọc errno. Nắm được cơ chế đó, phân biệt được hàm thư viện với system call, và viết sao cho khả chuyển, bạn đã có đủ quy ước để đọc trọn phần còn lại của cuốn sách.1
Chương trình muốn nhờ nhân thì phải qua một cái cửa gọi là system call, và lần nào cũng phải xem cửa có mở không. Hiểu luật chơi này là đủ nền để đi tiếp. Nhớ nhé: gõ cửa nhân, rồi luôn kiểm tra kết quả — đó là nếp của cả cuốn sách.
Systems programming begins at a boundary: your program knocks on the kernel through a system call, and every knock can fail — so always check the return value, then read errno. Grasp that mechanism, tell a library function from a system call, and write portably, and you have the conventions to read the rest of the book.1