Kĩ Thuật Lập Trình Hệ Thống Linux [Chương 3]
Chương 3 Những khái niệm lập trình hệ thống
Chương này bao gồm các chủ đề khác nhau là điều kiện tiên quyết cho lập trình hệ thống. Chúng ta bắt đầu bằng cách giới thiệu các lời gọi hệ thống và nêu chi tiết các bước xảy ra trong quá trình thực thi. Sau đó, chúng ta xem xét các hàm của thư viện và cách chúng khác với các lời gọi hệ thống, và cả hai với với mô tả của thư viện (GNU) C.
Bất cứ khi nào chúng ta thực hiện lời gọi hệ thống hoặc gọi hàm thư viện, chúng ta luôn nên kiểm tra trạng thái trả lại của lời gọi để xác định xem nó có thành công hay không. Chúng ta mô tả cách thực hiện các kiểm tra như vậy và trình bày một tập hợp các hàm được sử dụng trong hầu hết các chương trình mẫu trong sách này để chẩn đoán lỗi từ lời gọi hệ thống và hàm thư viện.
Chúng ta kết luận bằng cách xem xét các vấn đề khác nhau liên quan đến lập trình có tính tương thích, cụ thể là việc sử dụng các macro và các kiểu dữ liệu hệ thống tiêu chuẩn được xác định bởi SUSv3.
3.1 System calls
Một lời gọi hệ thống là một điểm đầu vào điều khiển kernel, cho phép một process yêu cầu kernel thực hiện một số hành động thay cho process. Kernel tạo một loạt các dịch vụ có thể truy cập vào các chương trình thông qua giao diện lập trình hệ thống (API). Các dịch vụ này bao gồm, tạo ra process mới, thực hiện I/O và tạo một đường ống cho giao tiếp giữa các process. (Trang hướng dẫn sử dụng syscalls (2) có liệt kê các lời gọi hệ thống Linux.)
Trước khi đi vào chi tiết về cách hoạt động của lời gọi hệ thống , chúng ta lưu ý một số thông tin chung:
- Lời gọi hệ thống thay đổi trạng thái bộ xử lý từ chế độ người dùng sang chế độ kernel, vì vậy CPU có thể truy cập bộ nhớ được kernel bảo vệ.
- Tập hợp các lời gọi hệ thống được cố định. Mỗi lời gọi hệ thống được xác định bằng một số duy nhất (Lược đồ đánh số này thường không hiển thị cho các chương trình, trong đó xác định lời gọi hệ thống theo tên.)
- Mỗi lời gọi hệ thống có thể có một bộ đối số chỉ định thông tin cần được chuyển từ không gian người dùng (nghĩa là không gian địa chỉ ảo của process) sang không gian kernel và ngược lại.
Từ quan điểm lập trình, việc gọi một lời gọi hệ thống giống như gọi một hàm C. Tuy nhiên, ngữ cảnh đằng sau, nhiều bước xảy ra trong quá trình thực thi một lời gọi hệ thống. Để minh họa điều này, chúng ta xem xét các bước theo thứ tự mà chúng xảy ra về việc triển khai phần cứng cụ thể, x86-32. Các bước thực hiện như sau:
- 1. Chương trình ứng dụng thực hiện lời gọi hệ thống bằng cách gọi hàm shell trong thư viện C.
- 2. Hàm wrapper làm cho tất cả các đối số của lời gọi hệ thống có sẵn cho hệ thống gọi là thủ tục xử lý trap. Các đối số này chuyển đến trình shell thông qua ngăn xếp, nhưng kernel mong đợi chúng trong những thanh ghi cụ thể. Hàm wrapper sao chép các đối số vào các thanh ghi này.
- 3. Vì tất cả các lời gọi hệ thống đi vào kernel theo cùng một cách, kernel cần một số phương pháp xác định lời gọi hệ thống. Để cho phép điều này, hàm wrapper sao chép số định danh lời gọi hệ thống vào một thanh ghi CPU cụ thể (% eax).
- 4. Hàm wrapper thực hiện một chuỗi lệnh máy (int 0x80), làm cho bộ xử lý chuyển từ chế độ người dùng sang chế độ kernel và thực thi mã được trỏ đến theo vị trí 0x80 (128 thập phân) của vectơ trap của hệ thống. Các kiến trúc x86-32 gần đây hơn thực hiện lệnh sysenter, cung cấp phương thức đi vào chế độ kernel nhanh hơn câu lệnh trap int 0x80 thông thường. Việc sử dụng sysenter được hỗ trợ trong kernel 2.6 và từ glibc 2.3.2 trở đi.
- 5. Để đáp ứng với trap tới vị trí 0x80, kernel gọi thủ tục system_call () của chính nó (nằm trong tập tin assembler arch /i386/entry.S) để xử lý trap. Chúng xử lý:
- a) Lưu các giá trị thanh ghi vào ngăn xếp kernel(Phần 6.5).
- b) Kiểm tra tính hợp lệ số định danh của lời gọi hệ thống.
- c) Gọi thủ tục lời gọi hệ thống thích hợp, được tìm thấy bởi sử dụng số định danh của lời gọi hệ thống để lập một bảng chỉ mục của tất cả các thủ tục lời gọi hệ thống (biến kernel sys_call_table). Nếu thủ tục lời gọi hệ thống có bất kỳ đối số nào, trước tiên nó kiểm tra tính hợp lệ của chúng; ví dụ, nó kiểm tra địa chỉ trỏ đến vị trí hợp lệ trong bộ nhớ người dùng. Sau đó thủ tục thực hiện tác vụ bắt buộc, có thể liên quan đến việc sửa đổi các giá trị tại địa chỉ được chỉ định trong các đối số đã cho và chuyển dữ liệu giữa bộ nhớ người dùng và bộ nhớ kernel (ví dụ: trong các hoạt động I/O). Cuối cùng thủ tục trả về trạng thái kết quả cho thường trình system_call ().
- d) Khôi phục các giá trị thanh ghi từ ngăn xếp kernel và đặt các giá trị trả về của lời gọi hệ thống trên ngăn xếp.
- e) Trả về hàm shell, đồng thời trả về bộ xử lý sang chế độ người dùng.
- 6. Nếu giá trị trả về của thủ tục lời gọi hệ thống chỉ ra một lỗi, hàm wrapper set biến toàn cục errno (xem Phần 3.4) với giá trị này. Hàm wrapper sau đó trả về cho người gọi, cung cấp một giá trị trả về số nguyên cho biết thành công hoặc thất bại của lời gọi hệ thống. Trên Linux,thủ tục lời gọi hệ thống theo một quy ước trả về giá trị không âm để biểu thị thành công. Trong trường hợp có lỗi, thường trình trả về số âm, là giá trị âm của một trong các hằng số errno. Khi một giá trị âm được trả về, hàm wrapper của thư viện C sẽ loại bỏ nó (để làm cho nó dương), sao chép kết quả vào errno, và trả về –1 làm hàm kết quả của trình shell để biểu thị lỗi cho chương trình gọi. Quy ước này dựa trên giả định rằng các thủ tục gọi hệ thống không trả về giá trị âm khi thành công. Tuy nhiên, đối với một số trong những thủ tục này, giả định này không được đảm bảo. Thông thường, đây không phải là vấn đề, vì phạm vi các giá trị errno phủ định không trùng lặp với các giá trị trả về âm. Tuy nhiên, quy ước này gây ra vấn đề trong một trường hợp: hoạt động F_GETOWN của lời gọi hệ thống fcntl (), mà chúng tôi mô tả trong Phần 63.3.
Hình 3-1 minh họa trình tự trên bằng cách sử dụng ví dụ về lời gọi hệ thống execve (). Trên Linux / x86-32, execve () là lời gọi hệ thống số 11 (__NR_execve). Như vậy, trong sys_call_table vector, mục 11 chứa địa chỉ của sys_execve (), thủ tục cho lời gọi hệ thống này. (Trên Linux, các thủ tục lời gọi hệ thống thường có tên của dạng sys_xyz (), trong đó xyz () là lời gọi hệ thống được đề cập.)
Thông tin được đưa ra trong các đoạn trên là nhiều hơn chúng ta cần biết phần còn lại của cuốn sách này. Tuy nhiên, nó là minh họa rất quan trọng, ngay cả đối với một cuộc gọi hệ thống đơn giản, khá nhiều công việc phải được thực hiện, và do đó các lời gọi hệ thống có chi phí nhỏ nhưng đáng giá. Ví dụ về chi phí thực hiện lời gọi hệ thống, hãy xem xét getppid () lời gọi hệ thống, chỉ đơn giản trả về process ID của process cha của process dùng lời gọi hệ thống. Trên một trong các hệ thống x86-32 của tác giả chạy Linux 2.6.25, 10 triệu các cuộc gọi đến getppid () cần khoảng 2,2 giây để hoàn thành. Chi phí này khoảng 0,3 micro giây cho mỗi cuộc gọi. Bằng cách so sánh, trên cùng một hệ thống, 10 triệu cuộc gọi đến một hàm C chỉ trả về một số nguyên bắt buộc 0,11 giây, hoặc khoảng một phần mười hai thời gian cần thiết cho các cuộc gọi đến getppid (). Tất nhiên, hầu hết các lời gọi hệ thống có chi phí cao hơn đáng kể so với getppid ().
Vì từ quan điểm của một chương trình C, việc gọi hàm C wrapper của thư viện đồng nghĩa với việc gọi thủ tục lời gọi hệ thống tương ứng, trong phần còn lại của cuốn sách này, chúng ta sử dụng từ ngữ như "lời gọi hệ thống gọi xyz ()" có nghĩa là "gọi hàm shell gọi hệ thống gọi xyz().
Phụ lục A mô tả lệnh strace, có thể được sử dụng để theo dõi các lời gọi hệ thống được thực hiện bởi một chương trình, cho mục đích gỡ lỗi hoặc chỉ đơn giản là điều tra những gì một chương trình đang làm. Thông tin thêm về cơ chế lời gọi hệ thống Linux có thể được tìm thấy trong [Love, 2010], [Bovet & Cesati, 2005] và [Maxwell, 1999].
3.2 Hàm thư viện
Một hàm thư viện đơn giản là một trong vô số các hàm cấu thành thư viện C chuẩn. (Cho ngắn gọn, khi nói về một hàm cụ thể trong phần còn lại của cuốn sách chúng ta thường chỉ viết hàm thay vì hàm thư viện.) Mục đích của các hàm này rất đa dạng, bao gồm các tác vụ như mở tệp, chuyển đổi đến một định dạng có thể đọc được và so sánh hai chuỗi ký tự.
Nhiều hàm thư viện không thực hiện bất kỳ lời gọi hệ thống nào (ví dụ: các hàm điều khiển chuỗi). Mặt khác, một số hàm thư viện được xếp là lớp trên của các lời gọi hệ thống. Ví dụ, hàm fopen () sử dụng lời gọi hệ thống open () để thực sự mở một tập tin. Thông thường, các chức năng thư viện được thiết kế để cung cấp thêm giao diện người gọi thân thiện hơn so với lời gọi hệ thống cơ bản. Ví dụ, hàm printf () cung cấp định dạng đầu ra và đệm dữ liệu, trong khi hệ thống write () chỉ cần kết quả đầu ra là một khối byte. Tương tự, các hàm malloc () và free () thực hiện các nhiệm vụ khác nhau giúp chúng dễ dàng phân bổ và giải phóng bộ nhớ hơn lệnh gọi hệ thống brk () cơ bản.
3.3 The Standard C Library; The GNU C Library (glibc)
Có các triển khai khác nhau của thư viện C chuẩn trên nhiều triển khai UNIX khác nhau. Việc triển khai phổ biến nhất được sử dụng trên Linux là GNU Thư viện C (glibc, http://www.gnu.org/software/libc/). Nhà phát triển chính và người duy trì thư viện GNU C ban đầu
Roland McGrath. Ngày nay, nhiệm vụ này được thực hiện bởi Ulrich Drepper. Nhiều thư viện C khác có sẵn cho Linux, bao gồm các thư viện với yêu cầu bộ nhớ nhỏ hơn để sử dụng trong các ứng dụng thiết bị nhúng. Ví dụ bao gồm uClibc (http://www.uclibc.org/) và diet libc (http://www.fefe.de/dietlibc/).
Trong cuốn sách này, chúng ta giới hạn thảo luận với glibc, vì đó là thư viện C được sử dụng bởi hầu hết các ứng dụng được phát triển trên Linux.
Xác định phiên bản glibc trên hệ thống
Đôi khi, chúng ta cần phải xác định phiên bản của glibc trên một hệ thống. Từ shell, chúng ta có thể làm điều này bằng cách chạy tập tin thư viện được chia sẻ glibc như thể nó là một chương trình thực thi. Khi chúng ta chạy thư viện dưới dạng tệp thực thi, thư viện sẽ hiển thị nhiều văn bản khác nhau,
bao gồm cả số phiên bản của nó:
$ /lib/libc.so.6
GNU C Library stable release version 2.10.1, by Roland McGrath et al.
Copyright (C) 2009 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 4.4.0 20090506 (Red Hat 4.4.0-4).
Compiled on a Linux >>2.6.18-128.4.1.el5<< system on 2009-08-19.
Available extensions:
The C stubs add-on version 2.1.2.
crypt add-on version 2.1 by Michael Glad and others
GNU Libidn by Simon Josefsson
Native POSIX Threads Library by Ulrich Drepper et al
BIND-8.2.3-T5B
RT using linux kernel aio
For bug reporting instructions, please see:
<http://www.gnu.org/software/libc/bugs.html>.
Trong một số bản phân phối Linux, thư viện GNU C nằm ở một tên đường dẫn khác với
/lib/libc.so.6. Một cách xác định vị trí của thư viện là chạy ldd (list dynamic dependencies) thực thi liên kết động của glibc (hầu hết các tập tin thực thi được liên kết theo cách này). Sau đó chúng ta có thể kiểm tra đường dẫn đến danh sách thư viện phụ thuộc để tìm vị trí của thư viện chia sẻ glibc:
$ ldd myprog | grep libc
libc.so.6 => /lib/tls/libc.so.6 (0x4004b000)
Có hai cách mà một chương trình ứng dụng có thể xác định phiên bản của thư viện GNU C hiện diện trên hệ thống: bằng cách kiểm tra các hằng số hoặc bằng cách gọi hàm thư viện. Từ phiên bản 2.0 trở đi, glibc định nghĩa hai hằng số, __GLIBC__ và __GLIBC_MINOR__, có thể được kiểm tra tại thời gian biên dịch (trong câu lệnh #ifdef). Trên một hệ thống với glibc 2.12 được cài đặt, các hằng số này sẽ có các giá trị 2 và 12. Tuy nhiên, các hằng số này được sử dụng hạn chế trong một chương trình được biên dịch trên một hệ thống nhưng chạy trên một hệ thống khác với glibc khác. Để xử lý khả năng này, một chương trình có thể gọi hàm gnu_get_libc_version () để xác định phiên bản
glibc có sẵn tại thời gian chạy.
#include <gnu/libc-version.h>
const char *gnu_get_libc_version(void);
Returns pointer to null-terminated, statically allocated string
containing GNU C library version number
Hàm gnu_get_libc_version () trả về một con trỏ tới một chuỗi, chẳng hạn như 2.12. Chúng ta cũng có thể lấy thông tin phiên bản bằng cách sử dụng hàm confstr () để truy lục giá trị của biến cấu hình _CS_GNU_LIBC_VERSION (glibc-specific). Cuộc gọi này trả về một chuỗi như "glibc 2.12"
3.4 Xử lý lỗi từ các cuộc gọi hệ thống và chức năng thư viện
Hầu như mọi hàm gọi và thư viện hệ thống đều trả về một số loại giá trị trạng thái cho biết cuộc gọi thành công hay thất bại. Giá trị trạng thái này luôn phải được kiểm tra xem lời gọi có thành công hay không. Nếu không, thì hành động thích hợp nên được thực hiện - ít nhất, chương trình sẽ hiển thị thông báo lỗi cảnh báo rằng có điều gì đó bất ngờ xảy ra.
Mặc dù nó là hấp dẫn để tiết kiệm thời gian đánh máy bằng cách loại trừ các kiểm tra này (đặc biệt là sau khi nhìn thấy ví dụ về các chương trình UNIX và Linux, nơi các giá trị trạng thái không được kiểm tra), đó là một nhận định sai lầm. Nhiều giờ gỡ lỗi có thể bị lãng phí vì kiểm tra không được thực hiện trên trạng thái trả về của một lời gọi hệ thống hoặc hàm thư viện vì nghĩ rằng "không thể thất bại." Một vài lời gọi hệ thống không bao giờ thất bại. Ví dụ, getpid () luôn trả về thành công ID của một tiến trình và _exit () luôn chấm dứt một tiến trình. Không cần thiết để kiểm tra giá trị trả lại từ các lời gọi hệ thống như vậy.
Xử lý lỗi lời gọi hệ thống
Trang hướng dẫn sử dụng cho mỗi lời gọi hệ thống có chú thích các giá trị trả lại có thể có của lời gọi, hiển thị giá trị nào cho biết lỗi. Thông thường, một lỗi được biểu thị bằng một return of –1. Do đó, một lời gọi hệ thống có thể được kiểm tra bằng mã như sau:
fd = open (tên đường dẫn, cờ, chế độ); / * hệ thống gọi để mở một tập tin * /
if (fd == -1) {
/ * Mã để xử lý lỗi * /
}
...
#include <gnu / libc-version.h>
const char * gnu_get_libc_version (void);
Trả về con trỏ tới chuỗi được phân bổ bằng tĩnh, được phân bổ tĩnh
chứa GNU C library version number
if (close (fd) == -1) {
/ * Mã để xử lý lỗi * /
}
Khi một cuộc gọi hệ thống không thành công, nó sẽ đặt biến số nguyên toàn cục errno thành một giá trị dương để xác định lỗi cụ thể. Bao gồm tệp tiêu đề <errno.h> cung cấp một khai báo errno, cũng như một tập hợp các hằng số cho các số lỗi khác nhau. Tất cả các tên tượng trưng này bắt đầu bằng E. Phần này hướng đến ERRORS trong mỗi trang hướng dẫn cung cấp một danh sách các giá trị errno có thể có thể được trả về bởi mỗi lời gọi hệ thống.
Dưới đây là một ví dụ đơn giản về việc sử dụng errno để chẩn đoán lỗi cuộc gọi hệ thống:
cnt = read (fd, buf, numbytes);
if (cnt == -1) {
if (errno == EINTR)
fprintf (stderr, "đọc bị ngắt bởi tín hiệu \ n");
else {
/ * Đã xảy ra lỗi khác * /
}
}
Các cuộc gọi hệ thống thành công và các hàm thư viện không bao giờ đặt lại errno thành 0, do đó biến này có thể có giá trị khác không phải là kết quả của lỗi từ cuộc gọi trước đó. Hơn nữa, SUSv3 cho phép một lời gọi hàm thành công để đặt errno thành một giá trị khác (mặc dù ít hàm làm điều này). Do đó, khi kiểm tra lỗi, chúng ta nên đầu tiên luôn kiểm tra xem giá trị trả về hàm có cho biết lỗi hay không và chỉ khi đó kiểm tra errno để xác định nguyên nhân của lỗi.
Một vài lời gọi hệ thống (ví dụ: getpriority ()) có thể trả về hợp lệ –1 khi thành công. Đến xác định xem có xảy ra lỗi trong các lời gọi như vậy hay không, chúng ta đặt errno thành 0 trước cuộc gọi, và sau đó kiểm tra nó sau đó. Nếu cuộc gọi trả về –1 và errno là nonzero, lỗi xảy ra. (Một câu lệnh tương tự cũng áp dụng cho một vài hàm thư viện.)
Một hành động phổ biến sau khi một cuộc gọi hệ thống không thành công là in một thông báo lỗi dựa trên giá trị errno. Hàm perror () và strerror () được cung cấp cho mục đích này. Hàm perror () in chuỗi được trỏ đến bởi đối số msg của nó, theo sau bởi một thông điệp tương ứng với giá trị hiện tại của errno.
#include <stdio.h>
void perror (const char * msg)
Một cách đơn giản để xử lý lỗi từ các cuộc gọi hệ thống sẽ như sau:
fd = open (tên đường dẫn, cờ, chế độ);
if (fd == -1) {
perror ("mở");
exit(EXIT_FAILURE);
}
Hàm strerror () trả về chuỗi lỗi tương ứng với số lỗi được đưa ra trong đối số errnum của nó.
#include <string.h>
char *strerror(int errnum);Returns pointer to error string corresponding to errnum
pid_t mypid;
mypid = getpid (); / * Trả về quá trình ID của quá trình gọi * /
printf ("PID của tôi là% ld \ n", (dài) mypid);
Chúng ta đưa ra một ngoại lệ cho kỹ thuật trên. Vì loại dữ liệu off_t có kích thước long long trong một số môi trường biên dịch, chúng ta đưa ra giá trị off_t cho loại này và sử dụng công cụ chỉ định% lld, như được mô tả trong Phần 5.10.
Chuẩn C99 định nghĩa công cụ sửa đổi độ dài z cho printf (), để chỉ ra rằng chuyển đổi số nguyên sau tương ứng với loại size_t hoặc ssize_t. Vì vậy, chúng ta có thể viết% zd thay vì sử dụng% ld. Mặc dù
specifier có sẵn trong glibc, chúng ta tránh nó bởi vì nó không có sẵn trên tất cả triển khai UNIX.
Chuẩn C99 cũng định nghĩa công cụ sửa đổi độ dài j, chỉ định rằng đối số tương ứng là kiểu intmax_t (hoặc uintmax_t), một kiểu số nguyên được đảm bảo đủ lớn để có thể đại diện cho một số nguyên của kiểu bất kỳ. Cuối cùng, việc sử dụng một (intmax_t) cast cộng với% jd specifier nên thay thế cụm từ (dài) cộng với chỉ số% ld làm cách tốt nhất để in số giá trị kiểu dữ liệu hệ thống, vì cách tiếp cận cũ cũng xử lý các giá trị lâu dài và bất kỳ loại số nguyên mở rộng nào như int128_t. Tuy nhiên, (một lần nữa) chúng ta tránh kỹ thuật này vì nó không thể thực hiện trên tất cả các triển khai UNIX.
struct sembuf {
unsigned short sem_num ; / * Số semaphore * /
short sem_op ; / * Hoạt động được thực hiện * /
short sem_flg ; / * Cờ hoạt động * /
};
Mặc dù SUSv3 chỉ định các cấu trúc như sembuf, điều quan trọng:
Do đó, nó không phải là di động khi sử dụng một khởi tạo cấu trúc như sau:
struct sembuf s = {3, -1, SEM_UNDO};
Mặc dù trình khởi tạo này sẽ hoạt động trên Linux, nó sẽ không hoạt động trên một triển khai khác, trong đó các trường trong cấu trúc sembuf được định nghĩa theo một thứ tự khác. Để khởi tạo các cấu trúc di động như vậy, chúng ta phải sử dụng các câu lệnh gán rõ ràng, như sau:
struct sembuf s;
s.sem_num = 3;
s.sem_op = -1;
s.sem_flg = SEM_UNDO;
Nếu chúng ta đang sử dụng C99, thì chúng ta có thể sử dụng cú pháp mới của ngôn ngữ đó cho cấu trúc initializers để viết một khởi tạo tương đương:
struct sembuf s = {.sem_num = 3, .sem_op = -1, .sem_flg = SEM_UNDO};
Các cân nhắc về thứ tự của các thành viên của các cấu trúc tiêu chuẩn cũng được áp dụng
nếu chúng ta muốn viết nội dung của một cấu trúc chuẩn vào một tập tin. Để thực hiện điều này một cách hợp lý, chúng ta không thể đơn giản viết một cấu trúc nhị phân. Thay vào đó, các trường cấu trúc phải được viết riêng lẻ (có thể dưới dạng văn bản) theo thứ tự được chỉ định.
#ifdef WCOREDUMP
/ * Sử dụng macro WCOREDUMP () * /
#endif
Đối với nhiều chức năng được chỉ định, POSIX.1-1990 yêu cầu header file <sys / types.h> được include trước bất kỳ tiêu đề nào khác được liên kết với hàm. Tuy nhiên, yêu cầu này là thừa, vì hầu hết các triển khai UNIX hiện đại không yêu cầu các ứng dụng include header file cho các chức năng này. Do đó, SUSv1 đã xóa yêu cầu này.
Thư viện C chuẩn cung cấp vô số hàm thư viện thực hiện nhiều nhiệm vụ. Một số hàm thư viện sử dụng các lời gọi hệ thống để thực hiện công việc của chúng những hàm khác thực hiện nhiệm vụ hoàn toàn trong không gian người dùng. Trên Linux, chuẩn C thông thường thư viện được sử dụng là glibc.
Hầu hết các lời gọi hệ thống và hàm thư viện đều trả về trạng thái cho biết lời gọi đã thành công hay thất bại. Phải luôn kiểm tra trạng thái trả về như vậy. Chúng ta đã giới thiệu một số hàm mà chúng ta đã triển khai để sử dụng trong các chương trình mẫu trong cuốn sách này. Các nhiệm vụ được thực hiện bởi các hàm này bao gồm chẩn đoán lỗi và phân tích đối số dòng lệnh. Chúng ta đã thảo luận các hướng dẫn và kỹ thuật khác nhau có thể giúp chúng ta viết các chương trình hệ thống di động chạy trên bất kỳ hệ thống tuân thủ tiêu chuẩn nào. Khi biên dịch một ứng dụng, chúng ta có thể xác định các macro khác nhau kiểm soát các định nghĩa được hiển thị bởi các header file. Điều này hữu ích nếu chúng ta muốn đảm bảo một chương trình phù hợp với một số tiêu chuẩn được xác định hoặc được thực hiện.
Chúng ta có thể cải thiện tính di động của các chương trình hệ thống bằng cách sử dụng dữ liệu hệ thống các kiểu được xác định trong các tiêu chuẩn khác nhau, chứ không phải là các kiểu C gốc. SUSv3 chỉ định nhiều kiểu dữ liệu hệ thống mà triển khai nên hỗ trợ và ứng dụng nên sử dụng.
char *strerror(int errnum);Returns pointer to error string corresponding to errnum
Chuỗi trả về bởi strerror () có thể được phân bổ tĩnh, có nghĩa là nó có thể bị ghi đè bởi các cuộc gọi tiếp theo tới strerror (). Nếu errnum chỉ định một số lỗi không xác định, strerror () trả về một chuỗi biểu mẫu Lỗi không xác định nnn. Trên một số triển khai khác, strerror () thay vào đó trả về NULL trong trường hợp này.
Bởi vì các hàm perror () và strerror () là nhạy cảm miền địa phương (Phần 10.4), lỗi mô tả được hiển thị bằng ngôn ngữ địa phương.
Xử lý lỗi từ các hàm thư viện
Các hàm thư viện khác nhau trả về các kiểu dữ liệu khác nhau và các giá trị khác nhau cho biết thất bại. (Kiểm tra trang hướng dẫn sử dụng cho từng hàm.) Với mục đích của chúng ta, hàm thư viện có thể được chia thành các loại sau:
- Một số hàm thư viện trả về thông tin lỗi giống hệt như cách gọi hệ thống: giá trị trả về -1, với errno cho biết lỗi cụ thể. Một ví dụ của một hàm như vậy là remove (), loại bỏ một tệp (bằng cách sử dụng lời gọi hệ thống unlink ()) hoặc một thư mục (sử dụng lời gọi hệ thống rmdir ()). Lỗi từ các hàm này có thể được chẩn đoán giống như lỗi từ các lời gọi hệ thống.
- Một số hàm thư viện trả về một giá trị khác với –1 dựa trên lỗi, tuy nhiên đặt errno để chỉ ra tình trạng lỗi cụ thể. Ví dụ, fopen () trả về một NULL con trỏ khi lỗi, và các thiết lập của errno phụ thuộc vào lời gọi hệ thống cơ bản không thành công. Các hàm perror () và strerror () có thể được sử dụng để chẩn đoán những lỗi này.
- Các hàm thư viện khác không sử dụng errno. Phương pháp xác định sự tồn tại và nguyên nhân của lỗi phụ thuộc vào hàm cụ thể và được ghi lại trong trang hướng dẫn sử dụng hàm. Đối với các hàm này, đó là một sai lầm khi sử dụng errno, perror () hoặc strerror () để chẩn đoán lỗi.
3.5 Ghi chú về những ví dụ sử dụng trong quyển sách này
Trong phần này, chúng ta mô tả các quy ước và tính năng khác nhau thường được sử dụng bằng các chương trình mẫu được trình bày trong cuốn sách này.
3.5.1 Command-Line Options and Arguments
Nhiều chương trình ví dụ trong sách này dựa trên các tùy chọn dòng lệnh và các đối số để xác định hành vi của chúng. Các tùy chọn dòng lệnh UNIX truyền thống bao gồm dấu gạch ngang ban đầu, một chữ cái xác định tùy chọn và đối số tùy chọn. (Tiện ích GNU cung cấp một cú pháp tùy chọn mở rộng bao gồm hai dấu gạch nối ban đầu, theo sau là một chuỗi xác định tùy chọn và đối số tùy chọn.) Để phân tích các tùy chọn này, chúng tôi sử dụng hàm getopt () tiêu chuẩn (được mô tả trong Phụ lục B).
#include <string.h>
char * strerror (int errnum);
Trả về con trỏ tới chuỗi lỗi tương ứng với các errnum. Mỗi chương trình ví dụ của chúng ta có cú pháp dòng lệnh không độc quyền cung cấp một cơ sở trợ giúp đơn giản cho người dùng: nếu được gọi với tùy chọn ––help, chương trình sẽ hiển thị thông báo sử dụng cho biết cú pháp cho các tùy chọn dòng lệnh và đối số.
3.5.2 Common Functions and Header Files
Hầu hết các chương trình ví dụ bao gồm tệp tiêu đề có chứa các định nghĩa bắt buộc và chúng cũng sử dụng một tập hợp các hàm phổ biến. Chúng ta thảo luận về tập tin tiêu đề và hàm trong phần này.
Tệp tiêu đề chung
Liệt kê 3-1 là tệp tiêu đề được sử dụng bởi gần như mọi chương trình trong cuốn sách này. Tệp tiêu đề này bao gồm nhiều tệp tiêu đề khác được nhiều chương trình ví dụ sử dụng, định nghĩa một kiểu dữ liệu Boolean, và định nghĩa các macro để tính mức tối thiểu và tối đa hai giá trị số. Sử dụng tệp tiêu đề này cho phép chúng ta tạo các chương trình ví dụ ngắn hơn một chút.
Listing 3-1: Header file used by most example programs––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– lib/tlpi_hdr.h#ifndef TLPI_HDR_H
#define TLPI_HDR_H /* Prevent accidental double inclusion */
#include <sys/types.h> /* Type definitions used by many programs */
#include <stdio.h> /* Standard I/O functions */
#include <stdlib.h> /* Prototypes of commonly used library functions, plus EXIT_SUCCESS and EXIT_FAILURE constants */
#include <unistd.h> /* Prototypes for many system calls */
#include <errno.h> /* Declares errno and defines error constants */
#include <string.h> /* Commonly used string-handling functions */
#include "get_num.h" /* Declares our functions for handling numeric
arguments (getInt(), getLong()) */
#include "error_functions.h" /* Declares our error-handling functions */
typedef enum { FALSE, TRUE } Boolean;
#define min(m,n) ((m) < (n) ? (m) : (n))
#define max(m,n) ((m) > (n) ? (m) : (n))
#endif––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– lib/tlpi_hdr.h
#define TLPI_HDR_H /* Prevent accidental double inclusion */
#include <sys/types.h> /* Type definitions used by many programs */
#include <stdio.h> /* Standard I/O functions */
#include <stdlib.h> /* Prototypes of commonly used library functions, plus EXIT_SUCCESS and EXIT_FAILURE constants */
#include <unistd.h> /* Prototypes for many system calls */
#include <errno.h> /* Declares errno and defines error constants */
#include <string.h> /* Commonly used string-handling functions */
#include "get_num.h" /* Declares our functions for handling numeric
arguments (getInt(), getLong()) */
#include "error_functions.h" /* Declares our error-handling functions */
typedef enum { FALSE, TRUE } Boolean;
#define min(m,n) ((m) < (n) ? (m) : (n))
#define max(m,n) ((m) > (n) ? (m) : (n))
#endif––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– lib/tlpi_hdr.h
Chức năng chẩn đoán lỗi
Để đơn giản hóa việc xử lý lỗi trong các chương trình mẫu của chúng tôi, chúng tôi sử dụng chẩn đoán lỗi các hàm có khai báo được hiển thị trong Liệt kê 3-2
Listing 3-2: Declarations for common error-handling functions––––––––––––––––––––––––––––––––––––––––––––––––––––– lib/error_functions.h#ifndef ERROR_FUNCTIONS_H
#define ERROR_FUNCTIONS_H
void errMsg(const char *format, ...);
#ifdef __GNUC__
/* This macro stops 'gcc -Wall' complaining that "control reaches
end of non-void function" if we use the following functions to
terminate main() or some other non-void function. */
#define NORETURN __attribute__ ((__noreturn__))
#else
#define NORETURN
#endif
void errExit(const char *format, ...) NORETURN ;
void err_exit(const char *format, ...) NORETURN ;
void errExitEN(int errnum, const char *format, ...) NORETURN ;
void fatal(const char *format, ...) NORETURN ;
void usageErr(const char *format, ...) NORETURN ;
void cmdLineErr(const char *format, ...) NORETURN ;
#endif––––––––––––––––––––––––––––––––––––––––––––––––––––– lib/error_functions.h
#define ERROR_FUNCTIONS_H
void errMsg(const char *format, ...);
#ifdef __GNUC__
/* This macro stops 'gcc -Wall' complaining that "control reaches
end of non-void function" if we use the following functions to
terminate main() or some other non-void function. */
#define NORETURN __attribute__ ((__noreturn__))
#else
#define NORETURN
#endif
void errExit(const char *format, ...) NORETURN ;
void err_exit(const char *format, ...) NORETURN ;
void errExitEN(int errnum, const char *format, ...) NORETURN ;
void fatal(const char *format, ...) NORETURN ;
void usageErr(const char *format, ...) NORETURN ;
void cmdLineErr(const char *format, ...) NORETURN ;
#endif––––––––––––––––––––––––––––––––––––––––––––––––––––– lib/error_functions.h
Để chẩn đoán lỗi từ các cuộc gọi hệ thống và hàm thư viện, chúng ta sử dụng errMsg (),
errExit (), err_exit () và errExitEN ().
#include "tlpi_hdr.h"
void errMsg(const char *format, ...);
void errExit(const char *format, ...);
void err_exit(const char *format, ...);
void errExitEN(int errnum, const char *format, ...);
Khi in các giá trị của một trong các kiểu dữ liệu hệ thống số được hiển thị trong Bảng 3-1 (ví dụ: pid_t và uid_t), chúng ta phải cẩn thận không bao gồm sự phụ thuộc biểu diễn trong lệnh printf (). Một sự phụ thuộc biểu diễn có thể xảy ra bởi vì các quy tắc đối số của C chuyển đổi các giá trị kiểu short thành int, nhưng để lại các giá trị của kiểu int và không thay đổi. Điều này có nghĩa là, tùy thuộc vào định nghĩa của hệ thống kiểu dữ liệu, hoặc int hoặc long được truyền trong lệnh printf (). Tuy nhiên, vì printf () không có cách nào để xác định các loại đối số của nó tại thời gian chạy, người gọi phải cung cấp thông tin này một cách rõ ràng bằng cách sử dụng trình định dạng% d hoặc% ld. Các vấn đề là chỉ đơn giản là mã hóa một trong những specifier trong printf () phụ thuộc triển khai. Giải pháp thông thường là sử dụng trình chỉ định% ld và luôn truyền giá trị tương ứng thành long, như sau:void errMsg(const char *format, ...);
void errExit(const char *format, ...);
void err_exit(const char *format, ...);
void errExitEN(int errnum, const char *format, ...);
Hàm errMsg () in một thông báo lỗi chuẩn. Danh sách đối số của nó là tương tự như đối với printf (), ngoại trừ việc ký tự kết thúc dòng mới sẽ tự động nối thêm vào chuỗi đầu ra. Hàm errMsg () in văn bản lỗi tương ứng với giá trị hiện tại của errno — điều này bao gồm tên lỗi, chẳng hạn như EPERM, cộng với mô tả lỗi được trả về bởi strerror () - theo sau là định dạng đầu ra được chỉ định trong danh sách đối số. Hàm errExit () hoạt động như errMsg (), nhưng cũng chấm dứt chương trình,
hoặc bằng cách gọi exit () hoặc, nếu biến môi trường EF_DUMPCORE được xác định bằng giá trị chuỗi không rỗng, bằng cách gọi abort () để tạo một tệp kết xuất lõi để sử dụng với trình gỡ rối. (Chúng tôi giải thích các tệp kết xuất lõi trong Phần 22.1.) Hàm err_exit () tương tự như errExit (), nhưng khác nhau ở hai khía cạnh:
- Nó không tuôn ra đầu ra tiêu chuẩn trước khi in thông báo lỗi.
- Nó chấm dứt quá trình bằng cách gọi _exit () thay vì exit (). Điều này làm cho quá trình chấm dứt mà không xả bộ đệm stdio hoặc gọi trình xử lý thoát.
Chi tiết về những khác biệt này trong hoạt động của err_exit () sẽ trở nên rõ ràng hơn ở Chương 25, nơi chúng ta mô tả sự khác biệt giữa _exit () và exit (), và xem xét việc điều trị các bộ đệm stdio và các trình xử lý thoát trong một child process được tạo bởi fork (). Bây giờ, chúng ta chỉ cần lưu ý rằng err_exit () đặc biệt hữu ích nếu chúng ta viết một hàm thư viện tạo ra một tiến trình con cần chấm dứt do lỗi. Việc chấm dứt này sẽ xảy ra mà không xả bỏ bản sao của các bộ đệm stdio(ví dụ: cha của quá trình gọi) và không yêu cầu trình xử lý thoát được thiết lập bởi cha. Hàm errExitEN () cũng giống như errExit (), ngoại trừ việc thay vì in văn bản lỗi tương ứng với giá trị hiện tại của errno, nó in văn bản tương ứng với số lỗi (do đó, hậu tố EN) được đưa ra trong đối số errnum. Chủ yếu, chúng tôi sử dụng errExitEN () trong các chương trình sử dụng API chủ đề POSIX.
Không giống như các lời gọi hệ thống UNIX truyền thống, trả về -1 theo lỗi, các chủ đề POSIX
hàm chẩn đoán lỗi bằng cách trả về một số định danh lỗi (tức là, một số dương của loại thường được đặt trong errno) là kết quả hàm của chúng. (Các hàm POSIX thread trả về 0 khi thành công.)
Chúng ta có thể chẩn đoán lỗi từ các hàm POSIX thread bằng cách sử dụng mã như vậy
như sau:
errno = pthread_create (& thread, NULL, func, & arg);
if (errno! = 0)
errExit ("pthread_create");
Tuy nhiên, cách tiếp cận này không hiệu quả vì errno được định nghĩa trong các chương trình luồng như một macro mở rộng thành một cuộc gọi hàm trả về một giá trị có thể sửa đổi được.
Vì vậy, mỗi lần sử dụng kết quả errno trong một cuộc gọi hàm. Hàm errExitEN () cho phép chúng ta
viết một mã tương đương hiệu quả hơn ở trên:
int s;
s = pthread_create (& thread, NULL, func, & arg);
if (s! = 0)
errExitEN (s, "pthread_create");
Trong thuật ngữ C, một lvalue là một biểu thức đề cập đến một vùng lưu trữ. Ví dụ phổ biến nhất của một lvalue là một định danh cho một biến. Một số toán tử phụ thuộc vào lvalue. Ví dụ: nếu p là con trỏ đến bộ nhớ khu vực, thì *p là một lvalue. Theo API POSIX thread , errno được định nghĩa lại
là một hàm trả về một con trỏ đến một vùng lưu trữ cụ thể cho luồng (xem Mục 31.3).
Để chẩn đoán các loại lỗi khác, chúng ta sử dụng fatal (), usageErr () và cmdLineErr ()
#include "tlpi_hdr.h"
void fatal(const char *format, ...);
void usageErr(const char *format, ...);
void cmdLineErr(const char *format, ...);
Hàm fatal () được sử dụng để chẩn đoán các lỗi chung, bao gồm các lỗi từ hàm thư viện không đặt errno. Danh sách đối số của nó giống như đối với printf (), ngoại trừ một ký tự kết thúc dòng mới được tự động nối vào chuỗi đầu ra. Nó in đầu ra được định dạng theo lỗi chuẩn và sau đó chấm dứt chương trình như với errExit ().
Hàm usageErr () được sử dụng để chẩn đoán lỗi trong đối số dòng lệnh. Nó lấy danh sách đối số theo kiểu printf () và in chuỗi Cách sử dụng: Dựa theo đầu ra được định dạng theo lỗi chuẩn, và sau đó chấm dứt chương trình bằng cách gọi exit (). (Một số chương trình mẫu trong cuốn sách này cung cấp phiên bản mở rộng của hàm usageErr () mở rộng, dưới tên usageError ().)
Hàm cmdLineErr () tương tự như usageErr (), nhưng được dùng để chẩn đoán lỗi trong các đối số dòng lệnh được chỉ định cho một chương trình. Việc triển khai các hàm chẩn đoán lỗi của chúng tôi được hiển thị trong Liệt kê 3-3
Listing 3-3: Error-handling functions used by all programs––––––––––––––––––––––––––––––––––––––––––––––––––––– lib/error_functions.c#include <stdarg.h>
#include "error_functions.h"
#include "tlpi_hdr.h"
#include "ename.c.inc" /* Defines ename and MAX_ENAME */
#ifdef __GNUC__
__attribute__ ((__noreturn__))
#endif
static void terminate(Boolean useExit3)
{
char *s;
/* Dump core if EF_DUMPCORE environment variable is defined and
is a nonempty string; otherwise call exit(3) or _exit(2),
depending on the value of 'useExit3'. */
s = getenv("EF_DUMPCORE");
if (s != NULL && *s != '\0')
abort();
else if (useExit3)
exit(EXIT_FAILURE);
else
_exit(EXIT_FAILURE);
}
#include "tlpi_hdr.h"
void fatal(const char *format, ...);
void usageErr(const char *format, ...);
void cmdLineErr(const char *format, ...);
System Programming Concepts 55static void outputError(Boolean useErr, int err, Boolean flushStdout, const char *format, va_list ap)
{
#define BUF_SIZE 500
char buf[BUF_SIZE], userMsg[BUF_SIZE], errText[BUF_SIZE];
vsnprintf(userMsg, BUF_SIZE, format, ap);
if (useErr)
snprintf(errText, BUF_SIZE, " [%s %s]",
(err > 0 && err <= MAX_ENAME) ?
ename[err] : "?UNKNOWN?", strerror(err));
else
snprintf(errText, BUF_SIZE, ":");
snprintf(buf, BUF_SIZE, "ERROR%s %s\n", errText, userMsg);
if (flushStdout)
fflush(stdout); /* Flush any pending stdout */
fputs(buf, stderr);
fflush(stderr); /* In case stderr is not line-buffered */
}
void errMsg(const char *format, ...)
{
va_list argList;
int savedErrno;
savedErrno = errno; /* In case we change it here */
va_start(argList, format);
outputError(TRUE, errno, TRUE, format, argList);
va_end(argList);
errno = savedErrno;
}
void errExit(const char *format, ...)
{
va_list argList;
va_start(argList, format);
outputError(TRUE, errno, TRUE, format, argList);
va_end(argList);
terminate(TRUE);
}
void err_exit(const char *format, ...)
{
va_list argList;
va_start(argList, format);
outputError(TRUE, errno, FALSE, format, argList);
va_end(argList);
terminate(FALSE);
}
void errExitEN(int errnum, const char *format, ...)
{
va_list argList;
va_start(argList, format);
outputError(TRUE, errnum, TRUE, format, argList);
va_end(argList);
terminate(TRUE);
}
void fatal(const char *format, ...)
{
va_list argList;
va_start(argList, format);
outputError(FALSE, 0, TRUE, format, argList);
va_end(argList);
terminate(TRUE);
}
void usageErr(const char *format, ...)
{
va_list argList;
fflush(stdout); /* Flush any pending stdout */
fprintf(stderr, "Usage: ");
va_start(argList, format);
vfprintf(stderr, format, argList);
va_end(argList);
fflush(stderr); /* In case stderr is not line-buffered */
exit(EXIT_FAILURE);
}
void cmdLineErr(const char *format, ...)
{
va_list argList;
fflush(stdout); /* Flush any pending stdout */
fprintf(stderr, "Command-line usage error: ");
va_start(argList, format);
vfprintf(stderr, format, argList);
va_end(argList);
fflush(stderr); /* In case stderr is not line-buffered */
exit(EXIT_FAILURE);
}––––––––––––––––––––––––––––––––––––––––––––––––––––– lib/error_functions. h
Tệp enames.c.inc được liệt kê trong Liệt kê 3-3 được hiển thị trong Liệt kê 3-4. Tệp này xác định
một mảng các chuỗi, ename, đó là các tên biểu tượng tương ứng với mỗi các giá trị errno có thể có. Các hàm xử lý lỗi của chúng ta sử dụng mảng này để in ra tên tượng trưng tương ứng với một số lỗi cụ thể. Đây là một cách để giải quyết sự thật rằng, chuỗi được trả về bởi strerror () không xác định hằng số biểu tượng tương ứng với thông báo lỗi của nó, mặt khác, các trang hướng dẫn mô tả lỗi sử dụng tên biểu tượng của chúng. In ra tên tượng trưng cho chúng ta một cách dễ dàng để tìm kiếm nguyên nhân lỗi trong các trang hướng dẫn sử dụng.
Nội dung của tệp ename.c.inc là kiến trúc cụ thể, bởi vì các giá trị errno thay đổi phần nào từ một kiến trúc phần cứng Linux sang cấu trúc phần cứng Linux khác. Phiên bản được hiển thị trong Liệt kê 3-4 là cho hệ thống Linux 2.6 / x86-32. Tệp này được tạo bằng tập lệnh (lib / Build_ename.sh) được bao gồm trong phân phối mã nguồn cho điều này sách. Kịch bản này có thể được sử dụng để xây dựng một phiên bản của ename.c.inc mà nên phù hợp với nền tảng phần cứng và phiên bản kernel cụ thể. Lưu ý rằng một số chuỗi trong mảng ename trống. Những điều này tương ứng với giá trị lỗi không sử dụng. Hơn nữa, một số chuỗi trong ename bao gồm hai lỗi phân biệt bằng dấu gạch chéo. Các chuỗi này tương ứng với các trường hợp có hai ký hiệu tên lỗi có cùng giá trị số.
Từ tệp ename.c.inc, chúng ta có thể thấy rằng lỗi EAGAIN và EWOULDBLOCK có cùng giá trị. (SUSv3 cho phép rõ ràng điều này, và các giá trị của các các hằng số giống nhau trên hầu hết, nhưng không phải tất cả, các hệ thống UNIX khác.) Các lỗi này được trả về bởi một lời gọi hệ thống trong các trường hợp mà thông thường chặn (tức là, buộc phải chờ trước khi hoàn thành), nhưng người gọi đã yêu cầu lời gọi hệ thống trả lại lỗi thay vì chặn. EAGAIN có nguồn gốc trên Hệ thống V, và đó là lỗi được trả lại cho các lời gọi hệ thống thực hiện I / O, semaphore, hoạt động của hàng đợi tin nhắn và khóa tệp (fcntl ()). EWOULDBLOCK có nguồn gốc trên BSD, và nó được trả về bằng cách khóa tập tin (flock ()) và các cuộc gọi hệ thống socketrelated.
Trong SUSv3, EWOULDBLOCK chỉ được đề cập trong thông số kỹ thuật của các giao diện khác nhau liên quan đến socket. Đối với các giao diện này, SUSv3 cho phép EAGAIN hoặc EWOULDBLOCK được trả lại bằng các lời gọi không chặn. Đối với tất cả các lời gọi không chặn khác, chỉ lỗi EAGAIN được chỉ định trong SUSv3.
––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– lib/ename.c.incstatic char *ename[] = {
/* 0 */ "",
/* 1 */ "EPERM", "ENOENT", "ESRCH", "EINTR", "EIO", "ENXIO", "E2BIG",
/* 8 */ "ENOEXEC", "EBADF", "ECHILD", "EAGAIN/EWOULDBLOCK", "ENOMEM",
/* 13 */ "EACCES", "EFAULT", "ENOTBLK", "EBUSY", "EEXIST", "EXDEV",
/* 19 */ "ENODEV", "ENOTDIR", "EISDIR", "EINVAL", "ENFILE", "EMFILE",
/* 25 */ "ENOTTY", "ETXTBSY", "EFBIG", "ENOSPC", "ESPIPE", "EROFS",
/* 31 */ "EMLINK", "EPIPE", "EDOM", "ERANGE", "EDEADLK/EDEADLOCK",
/* 36 */ "ENAMETOOLONG", "ENOLCK", "ENOSYS", "ENOTEMPTY", "ELOOP", "",
/* 42 */ "ENOMSG", "EIDRM", "ECHRNG", "EL2NSYNC", "EL3HLT", "EL3RST",
/* 48 */ "ELNRNG", "EUNATCH", "ENOCSI", "EL2HLT", "EBADE", "EBADR",
/* 54 */ "EXFULL", "ENOANO", "EBADRQC", "EBADSLT", "", "EBFONT", "ENOSTR",
/* 61 */ "ENODATA", "ETIME", "ENOSR", "ENONET", "ENOPKG", "EREMOTE",
/* 67 */ "ENOLINK", "EADV", "ESRMNT", "ECOMM", "EPROTO", "EMULTIHOP",
/* 73 */ "EDOTDOT", "EBADMSG", "EOVERFLOW", "ENOTUNIQ", "EBADFD",
/* 78 */ "EREMCHG", "ELIBACC", "ELIBBAD", "ELIBSCN", "ELIBMAX",
/* 83 */ "ELIBEXEC", "EILSEQ", "ERESTART", "ESTRPIPE", "EUSERS",
/* 88 */ "ENOTSOCK", "EDESTADDRREQ", "EMSGSIZE", "EPROTOTYPE",
/* 92 */ "ENOPROTOOPT", "EPROTONOSUPPORT", "ESOCKTNOSUPPORT",
/* 95 */ "EOPNOTSUPP/ENOTSUP", "EPFNOSUPPORT", "EAFNOSUPPORT",
/* 98 */ "EADDRINUSE", "EADDRNOTAVAIL", "ENETDOWN", "ENETUNREACH",
/* 102 */ "ENETRESET", "ECONNABORTED", "ECONNRESET", "ENOBUFS", "EISCONN",
/* 107 */ "ENOTCONN", "ESHUTDOWN", "ETOOMANYREFS", "ETIMEDOUT",
/* 111 */ "ECONNREFUSED", "EHOSTDOWN", "EHOSTUNREACH", "EALREADY",
/* 115 */ "EINPROGRESS", "ESTALE", "EUCLEAN", "ENOTNAM", "ENAVAIL",
/* 120 */ "EISNAM", "EREMOTEIO", "EDQUOT", "ENOMEDIUM", "EMEDIUMTYPE",
/* 125 */ "ECANCELED", "ENOKEY", "EKEYEXPIRED", "EKEYREVOKED",
/* 129 */ "EKEYREJECTED", "EOWNERDEAD", "ENOTRECOVERABLE", "ERFKILL"
};
#define MAX_ENAME 132––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– lib/ename.c.inc
#include "tlpi_hdr.h"
int getInt (const char * arg, int cờ, const char * tên);
dài getLong (const char * arg, int cờ, const char * tên);
Cả hai trở về arg chuyển đổi sang dạng số. Nếu đối số tên là khác NULL, nó phải chứa một chuỗi xác định đối số trong arg. Chuỗi này được bao gồm như một phần của bất kỳ thông báo lỗi nào được hiển thị bởi hàm. Đối số cờ cung cấp một số điều khiển hoạt động của hàm getInt () và Hàm getLong (). Theo mặc định, các hàm này mong đợi các chuỗi chứa số nguyên thập phân có dấu. Bởi ORing (|) một hoặc nhiều hằng số GN_ * được định nghĩa trong Liệt kê 3-5 thành cờ, chúng ta có thể chọn các cơ sở thay thế để chuyển đổi và hạn chế phạm vi của số để không âm hoặc lớn hơn 0. Việc triển khai các hàm getInt () và getLong () được cung cấp trong Liệt kê 3-6.
Mặc dù đối số cờ cho phép chúng ta thực thi kiểm tra phạm vi được mô tả trong văn bản chính, trong một số trường hợp, chúng tôi không yêu cầu kiểm tra như vậy trong các chương trình mẫu của chúng ta, mặc dù nó có vẻ hợp lý để làm như vậy. Ví dụ, trong Liệt kê 47-1, chúng ta không kiểm tra đối số giá trị init. Điều này có nghĩa là người dùng có thể chỉ định số âm là giá trị ban đầu cho một semaphore, điều này sẽ dẫn đến lỗi (ERANGE) trong lời gọi hệ thống semctl () tiếp theo, bởi vì một semaphore không thể có giá trị âm. Bỏ qua kiểm tra phạm vi trong các trường hợp như vậy cho phép chúng ta test không chỉ với việc sử dụng đúng các lời gọi hệ thống và hàm thư viện, mà còn để xem điều gì xảy ra khi các đối số không hợp lệ được cung cấp. Các ứng dụng trong thế giới thực thường sẽ áp đặt các kiểm tra mạnh hơn trên các đối số dòng lệnh của chúng.
Listing 3-5: Header file for get_num.c–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– lib/get_num.h#ifndef GET_NUM_H
#define GET_NUM_H
#define GN_NONNEG 01 /* Value must be >= 0 */
#define GN_GT_0 02 /* Value must be > 0 */
/* By default, integers are decimal */
#define GN_ANY_BASE 0100 /* Can use any base - like strtol(3) */
#define GN_BASE_8 0200 /* Value is expressed in octal */
#define GN_BASE_16 0400 /* Value is expressed in hexadecimal */
long getLong(const char *arg, int flags, const char *name);
int getInt(const char *arg, int flags, const char *name);
#endif–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– lib/get_num.hListing 3-6: Functions for parsing numeric command-line arguments–––––––––––––––––––––––––––––––––––––––––––––––––––––– lib/get_num.c#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <errno.h>
#include "get_num.h"
60 Chapter 3static void
gnFail(const char *fname, const char *msg, const char *arg, const char *name)
{
fprintf(stderr, "%s error", fname);
if (name != NULL)
fprintf(stderr, " (in %s)", name);
fprintf(stderr, ": %s\n", msg);
if (arg != NULL && *arg != '\0')
fprintf(stderr, " offending text: %s\n", arg);
exit(EXIT_FAILURE);
}
static long
getNum(const char *fname, const char *arg, int flags, const char *name)
{
long res;
char *endptr;
int base;
if (arg == NULL || *arg == '\0')
gnFail(fname, "null or empty string", arg, name);
base = (flags & GN_ANY_BASE) ? 0 : (flags & GN_BASE_8) ? 8 :
(flags & GN_BASE_16) ? 16 : 10;
errno = 0;
res = strtol(arg, &endptr, base);
if (errno != 0)
gnFail(fname, "strtol() failed", arg, name);
if (*endptr != '\0')
gnFail(fname, "nonnumeric characters", arg, name);
if ((flags & GN_NONNEG) && res < 0)
gnFail(fname, "negative value not allowed", arg, name);
if ((flags & GN_GT_0) && res <= 0)
gnFail(fname, "value must be > 0", arg, name);
return res;
}
long
getLong(const char *arg, int flags, const char *name)
{
return getNum("getLong", arg, flags, name);
}
int
getInt(const char *arg, int flags, const char *name)
{
long res;
res = getNum("getInt", arg, flags, name);
System Programming Concepts 61if (res > INT_MAX || res < INT_MIN)
gnFail("getInt", "integer out of range", arg, name);
return (int) res;
}–––––––––––––––––––––––––––––––––––––––––––––––––––––– lib/get_num.c
Đôi khi viết một ứng dụng di động, chúng ta có thể muốn các header file chỉ hiển thị các định nghĩa (hằng số, các nguyên mẫu hàm, vv) tuân theo một tiêu chuẩn cụ thể. Để làm điều này, chúng ta xác định một hoặc nhiều các macro được liệt kê bên dưới khi biên dịch chương trình. Một cách mà chúng ta có thể làm điều này là bằng cách xác định macro trong mã nguồn chương trình trước khi include bất kỳ header file:
#define _BSD_SOURCE 1
Ngoài ra, chúng ta có thể sử dụng tùy chọn –D cho trình biên dịch C:
$ cc -D_BSD_SOURCE prog.c
Macro có vẻ khó hiểu, nhưng điều đó có ý nghĩa nếu chúng ta xem xét những thứ từ quan điểm của việc triển khai. Việc triển khai quyết định xem tính năng nào có sẵn trong mỗi header file sẽ hiển thị, theo kiểm tra (với #if) giá trị mà ứng dụng đã xác định cho các macro này. Các macro sau đây được chỉ định theo các tiêu chuẩn có liên quan và do đó việc sử dụng các macro này có thể di chuyển đến tất cả các hệ thống hỗ trợ các tiêu chuẩn này:
_POSIX_SOURCE
Nếu được định nghĩa (với bất kỳ giá trị nào), hãy hiển thị các định nghĩa phù hợp với POSIX.1-1990
và ISO C (1990). Macro này được thay thế bởi _POSIX_C_SOURCE.
Các macro sau đây được liệt kê cụ thể là glibc:
cũng xác định _POSIX_C_SOURCE với giá trị 199506. Đặt rõ ràng macro này làm cho các định nghĩa BSD được ưu tiên trong một vài trường hợp xung đột tiêu chuẩn.
$ cc -D_POSIX_SOURCE -D_POSIX_C_SOURCE = 199506 \
-D_BSD_SOURCE -D_SVID_SOURCE prog.c
Tệp tiêu đề <features.h> và trang hướng dẫn feature_test_macros (7) cung cấp thêm thông tin về chính xác giá trị nào được gán cho từng macro thử nghiệm tính năng. _POSIX_C_SOURCE, _XOPEN_SOURCE và POSIX.1 / SUS
Chỉ các macro _POSIX_C_SOURCE và _XOPEN_SOURCE được chỉ định trong POSIX.1-2001 / SUSv3, yêu cầu các macro này phải được xác định bằng các giá trị 200112 và 600, tương ứng, trong các ứng dụng phù hợp. Đang xác định _POSIX_C_SOURCE 200112 cung cấp sự phù hợp với cơ sở POSIX.1-2001
$ cc -std = c99 -D_XOPEN_SOURCE = 600
Nguyên mẫu của từng chức năng được hiển thị trong cuốn sách này cho biết bất kỳ macro nào phải được xác định để sử dụng chức năng đó trong một chương trình được biên dịch với các tùy chọn trình biên dịch mặc định hoặc các tùy chọn trong cc vừa được hiển thị. Các trang hướng dẫn cung cấp mô tả chính xác hơn về macro cần thiết để lộ khai báo của từng hàm.
Để tránh các vấn đề về tính di động như vậy, SUSv3 chỉ định nhiều dữ liệu hệ thống tiêu chuẩn khác nhau các loại và yêu cầu thực hiện để xác định và sử dụng các loại này một cách thích hợp. Mỗi loại được định nghĩa bằng cách sử dụng tính năng C typedef. Ví dụ: pid_t kiểu dữ liệu được dùng để biểu diễn các process ID và trên Linux / x86-32 loại này được định nghĩa như sau:
typedef int pid_t;
Hầu hết các kiểu dữ liệu hệ thống tiêu chuẩn đều có tên kết thúc bằng _t. Nhiều kiểu trong số chúng được khai báo trong tệp tiêu đề <sys / types.h>, mặc dù một số được định nghĩa trong file header khác.
Một ứng dụng nên sử dụng các định nghĩa kiểu này để khai báo biến một cách có thể mà nó sử dụng. Ví dụ: câu lệnh sau sẽ cho phép ứng dụng thể hiện chính xác các ID quá trình trên bất kỳ hệ thống tuân thủ SUSv3 nào:
pid_t mypid;
Bảng 3-1 liệt kê một số loại dữ liệu hệ thống mà chúng ta sẽ gặp phải trong cuốn sách này. Việc triển khai có thể chọn loại cơ bản dưới dạng một số nguyên hoặc số thực.
Khi thảo luận về các kiểu dữ liệu trong Bảng 3-1 trong các chương sau, chúng ta sẽ thường nói rằng một số kiểu “là một loại số nguyên [được chỉ định bởi SUSv3].” Điều này có nghĩa SUSv3 yêu cầu kiểu được định nghĩa là số nguyên, nhưng không yêu cầu kiểu số nguyên gốc cụ thể (ví dụ: short, int hoặc long) được sử dụng. (Thông thường, chúng ta sẽ không nói loại dữ liệu gốc cụ thể nào được sử dụng để đại diện cho từng hệ thống các kiểu dữ liệu trong Linux, vì một ứng dụng di động nên được viết để nó không quan tâm loại dữ liệu nào được sử dụng.)
void fatal(const char *format, ...);
void usageErr(const char *format, ...);
void cmdLineErr(const char *format, ...);
Hàm fatal () được sử dụng để chẩn đoán các lỗi chung, bao gồm các lỗi từ hàm thư viện không đặt errno. Danh sách đối số của nó giống như đối với printf (), ngoại trừ một ký tự kết thúc dòng mới được tự động nối vào chuỗi đầu ra. Nó in đầu ra được định dạng theo lỗi chuẩn và sau đó chấm dứt chương trình như với errExit ().
Hàm usageErr () được sử dụng để chẩn đoán lỗi trong đối số dòng lệnh. Nó lấy danh sách đối số theo kiểu printf () và in chuỗi Cách sử dụng: Dựa theo đầu ra được định dạng theo lỗi chuẩn, và sau đó chấm dứt chương trình bằng cách gọi exit (). (Một số chương trình mẫu trong cuốn sách này cung cấp phiên bản mở rộng của hàm usageErr () mở rộng, dưới tên usageError ().)
Hàm cmdLineErr () tương tự như usageErr (), nhưng được dùng để chẩn đoán lỗi trong các đối số dòng lệnh được chỉ định cho một chương trình. Việc triển khai các hàm chẩn đoán lỗi của chúng tôi được hiển thị trong Liệt kê 3-3
Listing 3-3: Error-handling functions used by all programs––––––––––––––––––––––––––––––––––––––––––––––––––––– lib/error_functions.c#include <stdarg.h>
#include "error_functions.h"
#include "tlpi_hdr.h"
#include "ename.c.inc" /* Defines ename and MAX_ENAME */
#ifdef __GNUC__
__attribute__ ((__noreturn__))
#endif
static void terminate(Boolean useExit3)
{
char *s;
/* Dump core if EF_DUMPCORE environment variable is defined and
is a nonempty string; otherwise call exit(3) or _exit(2),
depending on the value of 'useExit3'. */
s = getenv("EF_DUMPCORE");
if (s != NULL && *s != '\0')
abort();
else if (useExit3)
exit(EXIT_FAILURE);
else
_exit(EXIT_FAILURE);
}
#include "tlpi_hdr.h"
void fatal(const char *format, ...);
void usageErr(const char *format, ...);
void cmdLineErr(const char *format, ...);
System Programming Concepts 55static void outputError(Boolean useErr, int err, Boolean flushStdout, const char *format, va_list ap)
{
#define BUF_SIZE 500
char buf[BUF_SIZE], userMsg[BUF_SIZE], errText[BUF_SIZE];
vsnprintf(userMsg, BUF_SIZE, format, ap);
if (useErr)
snprintf(errText, BUF_SIZE, " [%s %s]",
(err > 0 && err <= MAX_ENAME) ?
ename[err] : "?UNKNOWN?", strerror(err));
else
snprintf(errText, BUF_SIZE, ":");
snprintf(buf, BUF_SIZE, "ERROR%s %s\n", errText, userMsg);
if (flushStdout)
fflush(stdout); /* Flush any pending stdout */
fputs(buf, stderr);
fflush(stderr); /* In case stderr is not line-buffered */
}
void errMsg(const char *format, ...)
{
va_list argList;
int savedErrno;
savedErrno = errno; /* In case we change it here */
va_start(argList, format);
outputError(TRUE, errno, TRUE, format, argList);
va_end(argList);
errno = savedErrno;
}
void errExit(const char *format, ...)
{
va_list argList;
va_start(argList, format);
outputError(TRUE, errno, TRUE, format, argList);
va_end(argList);
terminate(TRUE);
}
void err_exit(const char *format, ...)
{
va_list argList;
va_start(argList, format);
outputError(TRUE, errno, FALSE, format, argList);
va_end(argList);
terminate(FALSE);
}
void errExitEN(int errnum, const char *format, ...)
{
va_list argList;
va_start(argList, format);
outputError(TRUE, errnum, TRUE, format, argList);
va_end(argList);
terminate(TRUE);
}
void fatal(const char *format, ...)
{
va_list argList;
va_start(argList, format);
outputError(FALSE, 0, TRUE, format, argList);
va_end(argList);
terminate(TRUE);
}
void usageErr(const char *format, ...)
{
va_list argList;
fflush(stdout); /* Flush any pending stdout */
fprintf(stderr, "Usage: ");
va_start(argList, format);
vfprintf(stderr, format, argList);
va_end(argList);
fflush(stderr); /* In case stderr is not line-buffered */
exit(EXIT_FAILURE);
}
void cmdLineErr(const char *format, ...)
{
va_list argList;
fflush(stdout); /* Flush any pending stdout */
fprintf(stderr, "Command-line usage error: ");
va_start(argList, format);
vfprintf(stderr, format, argList);
va_end(argList);
fflush(stderr); /* In case stderr is not line-buffered */
exit(EXIT_FAILURE);
}––––––––––––––––––––––––––––––––––––––––––––––––––––– lib/error_functions. h
Tệp enames.c.inc được liệt kê trong Liệt kê 3-3 được hiển thị trong Liệt kê 3-4. Tệp này xác định
một mảng các chuỗi, ename, đó là các tên biểu tượng tương ứng với mỗi các giá trị errno có thể có. Các hàm xử lý lỗi của chúng ta sử dụng mảng này để in ra tên tượng trưng tương ứng với một số lỗi cụ thể. Đây là một cách để giải quyết sự thật rằng, chuỗi được trả về bởi strerror () không xác định hằng số biểu tượng tương ứng với thông báo lỗi của nó, mặt khác, các trang hướng dẫn mô tả lỗi sử dụng tên biểu tượng của chúng. In ra tên tượng trưng cho chúng ta một cách dễ dàng để tìm kiếm nguyên nhân lỗi trong các trang hướng dẫn sử dụng.
Nội dung của tệp ename.c.inc là kiến trúc cụ thể, bởi vì các giá trị errno thay đổi phần nào từ một kiến trúc phần cứng Linux sang cấu trúc phần cứng Linux khác. Phiên bản được hiển thị trong Liệt kê 3-4 là cho hệ thống Linux 2.6 / x86-32. Tệp này được tạo bằng tập lệnh (lib / Build_ename.sh) được bao gồm trong phân phối mã nguồn cho điều này sách. Kịch bản này có thể được sử dụng để xây dựng một phiên bản của ename.c.inc mà nên phù hợp với nền tảng phần cứng và phiên bản kernel cụ thể. Lưu ý rằng một số chuỗi trong mảng ename trống. Những điều này tương ứng với giá trị lỗi không sử dụng. Hơn nữa, một số chuỗi trong ename bao gồm hai lỗi phân biệt bằng dấu gạch chéo. Các chuỗi này tương ứng với các trường hợp có hai ký hiệu tên lỗi có cùng giá trị số.
Từ tệp ename.c.inc, chúng ta có thể thấy rằng lỗi EAGAIN và EWOULDBLOCK có cùng giá trị. (SUSv3 cho phép rõ ràng điều này, và các giá trị của các các hằng số giống nhau trên hầu hết, nhưng không phải tất cả, các hệ thống UNIX khác.) Các lỗi này được trả về bởi một lời gọi hệ thống trong các trường hợp mà thông thường chặn (tức là, buộc phải chờ trước khi hoàn thành), nhưng người gọi đã yêu cầu lời gọi hệ thống trả lại lỗi thay vì chặn. EAGAIN có nguồn gốc trên Hệ thống V, và đó là lỗi được trả lại cho các lời gọi hệ thống thực hiện I / O, semaphore, hoạt động của hàng đợi tin nhắn và khóa tệp (fcntl ()). EWOULDBLOCK có nguồn gốc trên BSD, và nó được trả về bằng cách khóa tập tin (flock ()) và các cuộc gọi hệ thống socketrelated.
Trong SUSv3, EWOULDBLOCK chỉ được đề cập trong thông số kỹ thuật của các giao diện khác nhau liên quan đến socket. Đối với các giao diện này, SUSv3 cho phép EAGAIN hoặc EWOULDBLOCK được trả lại bằng các lời gọi không chặn. Đối với tất cả các lời gọi không chặn khác, chỉ lỗi EAGAIN được chỉ định trong SUSv3.
––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– lib/ename.c.incstatic char *ename[] = {
/* 0 */ "",
/* 1 */ "EPERM", "ENOENT", "ESRCH", "EINTR", "EIO", "ENXIO", "E2BIG",
/* 8 */ "ENOEXEC", "EBADF", "ECHILD", "EAGAIN/EWOULDBLOCK", "ENOMEM",
/* 13 */ "EACCES", "EFAULT", "ENOTBLK", "EBUSY", "EEXIST", "EXDEV",
/* 19 */ "ENODEV", "ENOTDIR", "EISDIR", "EINVAL", "ENFILE", "EMFILE",
/* 25 */ "ENOTTY", "ETXTBSY", "EFBIG", "ENOSPC", "ESPIPE", "EROFS",
/* 31 */ "EMLINK", "EPIPE", "EDOM", "ERANGE", "EDEADLK/EDEADLOCK",
/* 36 */ "ENAMETOOLONG", "ENOLCK", "ENOSYS", "ENOTEMPTY", "ELOOP", "",
/* 42 */ "ENOMSG", "EIDRM", "ECHRNG", "EL2NSYNC", "EL3HLT", "EL3RST",
/* 48 */ "ELNRNG", "EUNATCH", "ENOCSI", "EL2HLT", "EBADE", "EBADR",
/* 54 */ "EXFULL", "ENOANO", "EBADRQC", "EBADSLT", "", "EBFONT", "ENOSTR",
/* 61 */ "ENODATA", "ETIME", "ENOSR", "ENONET", "ENOPKG", "EREMOTE",
/* 67 */ "ENOLINK", "EADV", "ESRMNT", "ECOMM", "EPROTO", "EMULTIHOP",
/* 73 */ "EDOTDOT", "EBADMSG", "EOVERFLOW", "ENOTUNIQ", "EBADFD",
/* 78 */ "EREMCHG", "ELIBACC", "ELIBBAD", "ELIBSCN", "ELIBMAX",
/* 83 */ "ELIBEXEC", "EILSEQ", "ERESTART", "ESTRPIPE", "EUSERS",
/* 88 */ "ENOTSOCK", "EDESTADDRREQ", "EMSGSIZE", "EPROTOTYPE",
/* 92 */ "ENOPROTOOPT", "EPROTONOSUPPORT", "ESOCKTNOSUPPORT",
/* 95 */ "EOPNOTSUPP/ENOTSUP", "EPFNOSUPPORT", "EAFNOSUPPORT",
/* 98 */ "EADDRINUSE", "EADDRNOTAVAIL", "ENETDOWN", "ENETUNREACH",
/* 102 */ "ENETRESET", "ECONNABORTED", "ECONNRESET", "ENOBUFS", "EISCONN",
/* 107 */ "ENOTCONN", "ESHUTDOWN", "ETOOMANYREFS", "ETIMEDOUT",
/* 111 */ "ECONNREFUSED", "EHOSTDOWN", "EHOSTUNREACH", "EALREADY",
/* 115 */ "EINPROGRESS", "ESTALE", "EUCLEAN", "ENOTNAM", "ENAVAIL",
/* 120 */ "EISNAM", "EREMOTEIO", "EDQUOT", "ENOMEDIUM", "EMEDIUMTYPE",
/* 125 */ "ECANCELED", "ENOKEY", "EKEYEXPIRED", "EKEYREVOKED",
/* 129 */ "EKEYREJECTED", "EOWNERDEAD", "ENOTRECOVERABLE", "ERFKILL"
};
#define MAX_ENAME 132––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– lib/ename.c.inc
Functions for parsing numeric command-line arguments
Tệp tiêu đề trong Liệt kê 3-5 cung cấp khai báo hai hàm mà chúng ta thường xuyên sử dụng để phân tích các đối số dòng lệnh số nguyên: getInt () và getLong (). Ưu điểm chính của việc sử dụng các hàm này thay vì atoi (), atol () và strtol () là chúng cung cấp một số kiểm tra tính hợp lệ cơ bản của các đối số dạng số. Hàm getInt () và getLong () chuyển đổi chuỗi được trỏ tới bởi arg thành int hoặc long, tương ứng. Nếu arg không chứa chuỗi số nguyên hợp lệ (nghĩa là, chỉ có chữ số và ký tự + và -), sau đó các hàm này in thông báo lỗi và chấm dứt chương trình.#include "tlpi_hdr.h"
int getInt (const char * arg, int cờ, const char * tên);
dài getLong (const char * arg, int cờ, const char * tên);
Cả hai trở về arg chuyển đổi sang dạng số. Nếu đối số tên là khác NULL, nó phải chứa một chuỗi xác định đối số trong arg. Chuỗi này được bao gồm như một phần của bất kỳ thông báo lỗi nào được hiển thị bởi hàm. Đối số cờ cung cấp một số điều khiển hoạt động của hàm getInt () và Hàm getLong (). Theo mặc định, các hàm này mong đợi các chuỗi chứa số nguyên thập phân có dấu. Bởi ORing (|) một hoặc nhiều hằng số GN_ * được định nghĩa trong Liệt kê 3-5 thành cờ, chúng ta có thể chọn các cơ sở thay thế để chuyển đổi và hạn chế phạm vi của số để không âm hoặc lớn hơn 0. Việc triển khai các hàm getInt () và getLong () được cung cấp trong Liệt kê 3-6.
Mặc dù đối số cờ cho phép chúng ta thực thi kiểm tra phạm vi được mô tả trong văn bản chính, trong một số trường hợp, chúng tôi không yêu cầu kiểm tra như vậy trong các chương trình mẫu của chúng ta, mặc dù nó có vẻ hợp lý để làm như vậy. Ví dụ, trong Liệt kê 47-1, chúng ta không kiểm tra đối số giá trị init. Điều này có nghĩa là người dùng có thể chỉ định số âm là giá trị ban đầu cho một semaphore, điều này sẽ dẫn đến lỗi (ERANGE) trong lời gọi hệ thống semctl () tiếp theo, bởi vì một semaphore không thể có giá trị âm. Bỏ qua kiểm tra phạm vi trong các trường hợp như vậy cho phép chúng ta test không chỉ với việc sử dụng đúng các lời gọi hệ thống và hàm thư viện, mà còn để xem điều gì xảy ra khi các đối số không hợp lệ được cung cấp. Các ứng dụng trong thế giới thực thường sẽ áp đặt các kiểm tra mạnh hơn trên các đối số dòng lệnh của chúng.
Listing 3-5: Header file for get_num.c–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– lib/get_num.h#ifndef GET_NUM_H
#define GET_NUM_H
#define GN_NONNEG 01 /* Value must be >= 0 */
#define GN_GT_0 02 /* Value must be > 0 */
/* By default, integers are decimal */
#define GN_ANY_BASE 0100 /* Can use any base - like strtol(3) */
#define GN_BASE_8 0200 /* Value is expressed in octal */
#define GN_BASE_16 0400 /* Value is expressed in hexadecimal */
long getLong(const char *arg, int flags, const char *name);
int getInt(const char *arg, int flags, const char *name);
#endif–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– lib/get_num.hListing 3-6: Functions for parsing numeric command-line arguments–––––––––––––––––––––––––––––––––––––––––––––––––––––– lib/get_num.c#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <errno.h>
#include "get_num.h"
60 Chapter 3static void
gnFail(const char *fname, const char *msg, const char *arg, const char *name)
{
fprintf(stderr, "%s error", fname);
if (name != NULL)
fprintf(stderr, " (in %s)", name);
fprintf(stderr, ": %s\n", msg);
if (arg != NULL && *arg != '\0')
fprintf(stderr, " offending text: %s\n", arg);
exit(EXIT_FAILURE);
}
static long
getNum(const char *fname, const char *arg, int flags, const char *name)
{
long res;
char *endptr;
int base;
if (arg == NULL || *arg == '\0')
gnFail(fname, "null or empty string", arg, name);
base = (flags & GN_ANY_BASE) ? 0 : (flags & GN_BASE_8) ? 8 :
(flags & GN_BASE_16) ? 16 : 10;
errno = 0;
res = strtol(arg, &endptr, base);
if (errno != 0)
gnFail(fname, "strtol() failed", arg, name);
if (*endptr != '\0')
gnFail(fname, "nonnumeric characters", arg, name);
if ((flags & GN_NONNEG) && res < 0)
gnFail(fname, "negative value not allowed", arg, name);
if ((flags & GN_GT_0) && res <= 0)
gnFail(fname, "value must be > 0", arg, name);
return res;
}
long
getLong(const char *arg, int flags, const char *name)
{
return getNum("getLong", arg, flags, name);
}
int
getInt(const char *arg, int flags, const char *name)
{
long res;
res = getNum("getInt", arg, flags, name);
System Programming Concepts 61if (res > INT_MAX || res < INT_MIN)
gnFail("getInt", "integer out of range", arg, name);
return (int) res;
}–––––––––––––––––––––––––––––––––––––––––––––––––––––– lib/get_num.c
3.6 Portability Issues
Trong phần này, chúng ta xem xét chủ đề viết các chương trình hệ thống di động. Chúng ta giới thiệu các macro thử nghiệm và các kiểu dữ liệu hệ thống tiêu chuẩn được xác định bởi SUSv3, và sau đó xem xét một số vấn đề về tính di động khác.3.6.1 Feature Test Macros
Các tiêu chuẩn khác nhau điều chỉnh hành vi của lời gọi hệ thống và các hàm API (xem Phần 1.3). Một số tiêu chuẩn này được xác định bởi các cơ quan tiêu chuẩn như vậy The Open Group (Single UNIX Specification), trong khi các nhóm khác được xác định bởi hai triển khai UNIX quan trọng trong lịch sử: BSD và System V Release 4 ( and the associated System V Interface Definition).Đôi khi viết một ứng dụng di động, chúng ta có thể muốn các header file chỉ hiển thị các định nghĩa (hằng số, các nguyên mẫu hàm, vv) tuân theo một tiêu chuẩn cụ thể. Để làm điều này, chúng ta xác định một hoặc nhiều các macro được liệt kê bên dưới khi biên dịch chương trình. Một cách mà chúng ta có thể làm điều này là bằng cách xác định macro trong mã nguồn chương trình trước khi include bất kỳ header file:
#define _BSD_SOURCE 1
Ngoài ra, chúng ta có thể sử dụng tùy chọn –D cho trình biên dịch C:
$ cc -D_BSD_SOURCE prog.c
Macro có vẻ khó hiểu, nhưng điều đó có ý nghĩa nếu chúng ta xem xét những thứ từ quan điểm của việc triển khai. Việc triển khai quyết định xem tính năng nào có sẵn trong mỗi header file sẽ hiển thị, theo kiểm tra (với #if) giá trị mà ứng dụng đã xác định cho các macro này. Các macro sau đây được chỉ định theo các tiêu chuẩn có liên quan và do đó việc sử dụng các macro này có thể di chuyển đến tất cả các hệ thống hỗ trợ các tiêu chuẩn này:
_POSIX_SOURCE
Nếu được định nghĩa (với bất kỳ giá trị nào), hãy hiển thị các định nghĩa phù hợp với POSIX.1-1990
và ISO C (1990). Macro này được thay thế bởi _POSIX_C_SOURCE.
_POSIX_C_SOURCE
Nếu được xác định bằng giá trị 1, điều này có cùng tác dụng với _POSIX_SOURCE. Nếu được định nghĩa với một giá trị lớn hơn hoặc bằng 199309, cũng hiển thị các định nghĩa cho POSIX.1b (thời gian thực). Nếu được xác định với một giá trị lớn hơn hoặc bằng đến 199506, cũng hiển thị các định nghĩa cho POSIX.1c (chủ đề). Nếu được xác định bằng giá trị 200112, cũng hiển thị các định nghĩa cho đặc tả cơ sở POSIX.1-2001 (tức là, phần mở rộng XSI được loại trừ). (Trước phiên bản 2.3.3, tiêu đề glibc không diễn giải giá trị 200112 cho _POSIX_C_SOURCE.) Nếu được xác định với giá trị 200809, cũng hiển thị các định nghĩa cho POSIX.1-2008 đặc tả cơ sở. (Trước phiên bản 2.10, tiêu đề glibc không diễn giải giá trị 200809 cho _POSIX_C_SOURCE.)_XOPEN_SOURCE
Nếu được xác định (với bất kỳ giá trị nào), hiển thị POSIX.1, POSIX.2 và X / Open (XPG4) định nghĩa. Nếu được xác định với giá trị 500 hoặc lớn hơn, Các phần mở rộng SUSv2 (UNIX 98 và XPG5). Việc đặt thành 600 hoặc lớn hơn sẽ hiển thị thêm các mở rộng SUSv3 XSI (UNIX 03) và phần mở rộng C99. (Trước phiên bản 2.2, tiêu đề glibc không diễn giải giá trị 600 cho _XOPEN_SOURCE). Thiết lập đến 700 hoặc lớn hơn cũng cho thấy các phần mở rộng SUSv4 XSI. (Trước phiên bản 2.10, tiêu đề glibc không diễn giải giá trị 700 cho _XOPEN_SOURCE). Các giá trị 500, 600 và 700 cho _XOPEN_SOURCE đã được chọn vì SUSv2, SUSv3 và SUSv4 là các vấn đề tương ứng 5, 6 và 7 của X / Open thông số kỹ thuật.Các macro sau đây được liệt kê cụ thể là glibc:
_BSD_SOURCE
Nếu được định nghĩa (với bất kỳ giá trị nào), hãy hiển thị các định nghĩa BSD. Xác định macro nàycũng xác định _POSIX_C_SOURCE với giá trị 199506. Đặt rõ ràng macro này làm cho các định nghĩa BSD được ưu tiên trong một vài trường hợp xung đột tiêu chuẩn.
_SVID_SOURCE
Nếu được định nghĩa (với bất kỳ giá trị nào), hãy hiển thị Định nghĩa Giao diện Hệ thống V (SVID)_GNU_SOURCE
Nếu được xác định (với bất kỳ giá trị nào), hãy hiển thị tất cả các định nghĩa được cung cấp bởi cài đặt tất cả các macro trước, cũng như các phần mở rộng GNU khác nhau. Khi trình biên dịch GNU C được gọi mà không có các tùy chọn đặc biệt, _POSIX_SOURCE, _POSIX_C_SOURCE = 200809 (200112 với các phiên bản glibc 2.5 đến 2.9 hoặc 199506 với glibc phiên bản cũ hơn 2.4), _BSD_SOURCE và _SVID_SOURCE được xác định theo mặc định. Nếu các macro riêng lẻ được xác định hoặc trình biên dịch được gọi ở một trong các chế độ chuẩn của nó (ví dụ: cc –ansi hoặc cc –std = c99), thì chỉ các định nghĩa được yêu cầu mới là được cung cấp. Có một ngoại lệ: nếu _POSIX_C_SOURCE không được định nghĩa khác và trình biên dịch không được gọi ở một trong các chế độ tiêu chuẩn của nó, sau đó _POSIX_C_SOURCE là được xác định với giá trị 200809 (200112 với các phiên bản glibc 2.4 đến 2.9 hoặc 199506 với phiên bản glibc sớm hơn 2.4).$ cc -D_POSIX_SOURCE -D_POSIX_C_SOURCE = 199506 \
-D_BSD_SOURCE -D_SVID_SOURCE prog.c
Tệp tiêu đề <features.h> và trang hướng dẫn feature_test_macros (7) cung cấp thêm thông tin về chính xác giá trị nào được gán cho từng macro thử nghiệm tính năng. _POSIX_C_SOURCE, _XOPEN_SOURCE và POSIX.1 / SUS
Chỉ các macro _POSIX_C_SOURCE và _XOPEN_SOURCE được chỉ định trong POSIX.1-2001 / SUSv3, yêu cầu các macro này phải được xác định bằng các giá trị 200112 và 600, tương ứng, trong các ứng dụng phù hợp. Đang xác định _POSIX_C_SOURCE 200112 cung cấp sự phù hợp với cơ sở POSIX.1-2001
Feature test macros in function prototypes and source code examples
Các trang hướng dẫn mô tả (các) macro phải được xác định để làm cho một định nghĩa liên tục cụ thể hoặc khai báo hàm có thể nhìn thấy từ một header file. Tất cả các ví dụ mã nguồn trong cuốn sách này được viết để chúng biên dịch bằng cách sử dụng tùy chọn trình biên dịch GNU C mặc định hoặc các tùy chọn sau:$ cc -std = c99 -D_XOPEN_SOURCE = 600
Nguyên mẫu của từng chức năng được hiển thị trong cuốn sách này cho biết bất kỳ macro nào phải được xác định để sử dụng chức năng đó trong một chương trình được biên dịch với các tùy chọn trình biên dịch mặc định hoặc các tùy chọn trong cc vừa được hiển thị. Các trang hướng dẫn cung cấp mô tả chính xác hơn về macro cần thiết để lộ khai báo của từng hàm.
3.6.2 System Data Types
Các kiểu dữ liệu triển khai khác nhau được thể hiện bằng cách sử dụng các kiểu C chuẩn, ví dụ, process ID, ID người dùng và file offset. Mặc dù nó sẽ có thể sử dụng các kiểu C cơ bản như int và long để khai báo các biến lưu trữ thông tin như vậy, điều này làm giảm tính di động trên các hệ thống UNIX, vì những lý do sau:- Các kích thước của các loại cơ bản này thay đổi theo các triển khai UNIX (ví dụ: một long có thể là 4 byte trên một hệ thống và 8 byte trên một hệ thống khác), hoặc đôi khi ngay cả trong các môi trường biên dịch khác nhau trên cùng một triển khai. Hơn nữa, các triển khai khác nhau có thể sử dụng các loại khác nhau để biểu diễn cùng một thông tin. Ví dụ, một process ID có thể là một int trên một hệ thống nhưng là long trên một hệ thống khác.
- Ngay cả trên một triển khai UNIX đơn lẻ, các kiểu được sử dụng để biểu diễn thông tin có thể khác nhau giữa các bản phát hành. Các ví dụ đáng chú ý trên Linux là ID người dùng và nhóm. Trên Linux 2.2 và trước đó, các giá trị này được biểu diễn bằng 16 bit. Trên Linux 2.4 trở lên, chúng là các giá trị 32 bit.
Để tránh các vấn đề về tính di động như vậy, SUSv3 chỉ định nhiều dữ liệu hệ thống tiêu chuẩn khác nhau các loại và yêu cầu thực hiện để xác định và sử dụng các loại này một cách thích hợp. Mỗi loại được định nghĩa bằng cách sử dụng tính năng C typedef. Ví dụ: pid_t kiểu dữ liệu được dùng để biểu diễn các process ID và trên Linux / x86-32 loại này được định nghĩa như sau:
typedef int pid_t;
Hầu hết các kiểu dữ liệu hệ thống tiêu chuẩn đều có tên kết thúc bằng _t. Nhiều kiểu trong số chúng được khai báo trong tệp tiêu đề <sys / types.h>, mặc dù một số được định nghĩa trong file header khác.
Một ứng dụng nên sử dụng các định nghĩa kiểu này để khai báo biến một cách có thể mà nó sử dụng. Ví dụ: câu lệnh sau sẽ cho phép ứng dụng thể hiện chính xác các ID quá trình trên bất kỳ hệ thống tuân thủ SUSv3 nào:
pid_t mypid;
Bảng 3-1 liệt kê một số loại dữ liệu hệ thống mà chúng ta sẽ gặp phải trong cuốn sách này. Việc triển khai có thể chọn loại cơ bản dưới dạng một số nguyên hoặc số thực.
Printing system data type values
pid_t mypid;
mypid = getpid (); / * Trả về quá trình ID của quá trình gọi * /
printf ("PID của tôi là% ld \ n", (dài) mypid);
Chúng ta đưa ra một ngoại lệ cho kỹ thuật trên. Vì loại dữ liệu off_t có kích thước long long trong một số môi trường biên dịch, chúng ta đưa ra giá trị off_t cho loại này và sử dụng công cụ chỉ định% lld, như được mô tả trong Phần 5.10.
Chuẩn C99 định nghĩa công cụ sửa đổi độ dài z cho printf (), để chỉ ra rằng chuyển đổi số nguyên sau tương ứng với loại size_t hoặc ssize_t. Vì vậy, chúng ta có thể viết% zd thay vì sử dụng% ld. Mặc dù
specifier có sẵn trong glibc, chúng ta tránh nó bởi vì nó không có sẵn trên tất cả triển khai UNIX.
Chuẩn C99 cũng định nghĩa công cụ sửa đổi độ dài j, chỉ định rằng đối số tương ứng là kiểu intmax_t (hoặc uintmax_t), một kiểu số nguyên được đảm bảo đủ lớn để có thể đại diện cho một số nguyên của kiểu bất kỳ. Cuối cùng, việc sử dụng một (intmax_t) cast cộng với% jd specifier nên thay thế cụm từ (dài) cộng với chỉ số% ld làm cách tốt nhất để in số giá trị kiểu dữ liệu hệ thống, vì cách tiếp cận cũ cũng xử lý các giá trị lâu dài và bất kỳ loại số nguyên mở rộng nào như int128_t. Tuy nhiên, (một lần nữa) chúng ta tránh kỹ thuật này vì nó không thể thực hiện trên tất cả các triển khai UNIX.
3.6.3 Miscellaneous Portability Issues
Trong phần này, chúng ta xem xét một số vấn đề về tính di động khác mà chúng ta có thể gặp phải khi viết chương trình hệ thống.Khởi tạo và sử dụng cấu trúc
Mỗi bản thực thi UNIX chỉ định một loạt các cấu trúc tiêu chuẩn được sử dụng trong các lời gọi hệ thống và hàm thư viện khác nhau. Ví dụ, hãy xem xét cấu trúc sembuf, được sử dụng để đại diện cho một hoạt động semaphore được thực hiện bởi semop ():struct sembuf {
unsigned short sem_num ; / * Số semaphore * /
short sem_op ; / * Hoạt động được thực hiện * /
short sem_flg ; / * Cờ hoạt động * /
};
Mặc dù SUSv3 chỉ định các cấu trúc như sembuf, điều quan trọng:
- Nói chung, thứ tự các định nghĩa trường trong các cấu trúc như vậy không được xác định.
- Trong một số trường hợp, các trường bổ sung cụ thể có thể được bao gồm trong cấu trúc.
Do đó, nó không phải là di động khi sử dụng một khởi tạo cấu trúc như sau:
struct sembuf s = {3, -1, SEM_UNDO};
Mặc dù trình khởi tạo này sẽ hoạt động trên Linux, nó sẽ không hoạt động trên một triển khai khác, trong đó các trường trong cấu trúc sembuf được định nghĩa theo một thứ tự khác. Để khởi tạo các cấu trúc di động như vậy, chúng ta phải sử dụng các câu lệnh gán rõ ràng, như sau:
struct sembuf s;
s.sem_num = 3;
s.sem_op = -1;
s.sem_flg = SEM_UNDO;
Nếu chúng ta đang sử dụng C99, thì chúng ta có thể sử dụng cú pháp mới của ngôn ngữ đó cho cấu trúc initializers để viết một khởi tạo tương đương:
struct sembuf s = {.sem_num = 3, .sem_op = -1, .sem_flg = SEM_UNDO};
Các cân nhắc về thứ tự của các thành viên của các cấu trúc tiêu chuẩn cũng được áp dụng
nếu chúng ta muốn viết nội dung của một cấu trúc chuẩn vào một tập tin. Để thực hiện điều này một cách hợp lý, chúng ta không thể đơn giản viết một cấu trúc nhị phân. Thay vào đó, các trường cấu trúc phải được viết riêng lẻ (có thể dưới dạng văn bản) theo thứ tự được chỉ định.
Using macros that may not be present on all implementations
Trong một số trường hợp, một macro có thể không được định nghĩa trên tất cả các triển khai UNIX. Ví dụ, macro WCOREDUMP () (kiểm tra xem một tiến trình con có tạo ra một tệp core dump) có sẵn rộng rãi nhưng không được chỉ định trong SUSv3. Do đó, macro này có thể không có mặt trên một số triển khai UNIX. Để di chuyển được xử lý các khả năng như vậy, chúng ta có thể sử dụng chỉ thị tiền xử lý #ifdef của C, như trong ví dụ sau:#ifdef WCOREDUMP
/ * Sử dụng macro WCOREDUMP () * /
#endif
Variation in required header files across implementations
Trong một số trường hợp, các header file là cần thiết để thử nghiệm các lời gọi hệ thống khác nhau và các hàm thư viện khác nhau trên các triển khai UNIX. Trong cuốn sách này, chúng ta cho thấy yêu cầu trên Linux và lưu ý bất kỳ biến thể nào từ SUSv3. Một số tóm tắt hàm trong sách này hiển thị một header file cụ thể với chú thích đi kèm / * Dành cho tính di động * /. Điều này chỉ ra rằng header file không được yêu cầu trên Linux hoặc bởi SUSv3, nhưng vì một số triển khai khác (đặc biệt là cũ hơn) có thể yêu cầu nó, chúng ta nên include nó trong các chương trình di động.Đối với nhiều chức năng được chỉ định, POSIX.1-1990 yêu cầu header file <sys / types.h> được include trước bất kỳ tiêu đề nào khác được liên kết với hàm. Tuy nhiên, yêu cầu này là thừa, vì hầu hết các triển khai UNIX hiện đại không yêu cầu các ứng dụng include header file cho các chức năng này. Do đó, SUSv1 đã xóa yêu cầu này.
3.7 Kết luận
Các lời gọi hệ thống cho phép các process yêu cầu dịch vụ từ kernel. Ngay cả các lời gọi hệ thống đơn giản nhất cũng có chi phí đáng kể khi so sánh với không gian người dùng gọi hàm, vì hệ thống phải tạm thời chuyển sang chế độ kernel để thực thi lời gọi hệ thống và hạt nhân phải xác minh đối số và chuyển dữ liệu lời gọi hệ thống giữa bộ nhớ người dùng và bộ nhớ kernel.Thư viện C chuẩn cung cấp vô số hàm thư viện thực hiện nhiều nhiệm vụ. Một số hàm thư viện sử dụng các lời gọi hệ thống để thực hiện công việc của chúng những hàm khác thực hiện nhiệm vụ hoàn toàn trong không gian người dùng. Trên Linux, chuẩn C thông thường thư viện được sử dụng là glibc.
Hầu hết các lời gọi hệ thống và hàm thư viện đều trả về trạng thái cho biết lời gọi đã thành công hay thất bại. Phải luôn kiểm tra trạng thái trả về như vậy. Chúng ta đã giới thiệu một số hàm mà chúng ta đã triển khai để sử dụng trong các chương trình mẫu trong cuốn sách này. Các nhiệm vụ được thực hiện bởi các hàm này bao gồm chẩn đoán lỗi và phân tích đối số dòng lệnh. Chúng ta đã thảo luận các hướng dẫn và kỹ thuật khác nhau có thể giúp chúng ta viết các chương trình hệ thống di động chạy trên bất kỳ hệ thống tuân thủ tiêu chuẩn nào. Khi biên dịch một ứng dụng, chúng ta có thể xác định các macro khác nhau kiểm soát các định nghĩa được hiển thị bởi các header file. Điều này hữu ích nếu chúng ta muốn đảm bảo một chương trình phù hợp với một số tiêu chuẩn được xác định hoặc được thực hiện.
Chúng ta có thể cải thiện tính di động của các chương trình hệ thống bằng cách sử dụng dữ liệu hệ thống các kiểu được xác định trong các tiêu chuẩn khác nhau, chứ không phải là các kiểu C gốc. SUSv3 chỉ định nhiều kiểu dữ liệu hệ thống mà triển khai nên hỗ trợ và ứng dụng nên sử dụng.
Nhận xét
Đăng nhận xét