Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 42.126 phút đọc

Ranh giới user và kernel, errno

Sau bài này bạn sẽ làm được

  • Vẽ đường đi của một lời gọi từ user xuống kernel
  • Giải thích chi phí một syscall
  • Áp dụng ba quy tắc về errno
  • Xử lý các mã lỗi thường gặp

System call là cổng giao tiếp duy nhất giữa chương trình của bạn và nhân. Mọi việc chạm tới phần cứng, tệp, mạng, tiến trình đều đi qua đó. Bài này vẽ đường đi, đo cái giá của nó, và dạy cách đọc lỗi qua errno.

#Ranh giới user và kernel

  USER SPACE (che do nguoi dung, ring 3)
  +----------------------------------------+
  |  Chuong trinh cua ban                  |
  |    printf("hi")                        |
  |       |                                |
  |    libc: fwrite -> write()             |
  +-------|--------------------------------+
          |  lenh CPU: syscall (hoac int 0x80)
          |  chuyen sang che do KERNEL (ring 0)
  +-------v--------------------------------+
  |  KERNEL SPACE                          |
  |    sys_write() -> VFS -> driver -> phan cung
  +----------------------------------------+
System call
Một yêu cầu từ chương trình gửi tới nhân để làm một việc mà chương trình không được phép tự làm: đọc đĩa, gửi gói mạng, tạo tiến trình. CPU chuyển từ chế độ người dùng sang chế độ nhân, nhân làm việc, rồi trả về.

#Chi phí một syscall

terminal
# Linux: xem MOI syscall chuong trinh goi
strace ./hello
execve("./hello", ["./hello"], ...) = 0
brk(NULL) = 0x55...
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY) = 3
...
write(1, "hi\n", 3) = 3
exit_group(0) = ?

#errno

errno_thu.c
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>

int main(void) {
    errno = 0;                              /* dat lai TRUOC khi goi */
    int fd = open("khong_ton_tai_12345.txt", O_RDONLY);
    if (fd < 0) {
        printf("open that bai\n");
        perror("  perror");                 /* in thong diep he thong */
        printf("  errno=%d: %s\n", errno, strerror(errno));
        if (errno == ENOENT)
            printf("  -> ENOENT: tep khong ton tai\n");
    }
    return 0;
}
terminal
gcc -Wall -Wextra errno_thu.c -o errno_thu.exe && ./errno_thu.exe
open that bai
  perror: No such file or directory
  errno=2: No such file or directory
  -> ENOENT: tep khong ton tai

#Ba quy tắc

Đọc errno khi hàm thành công
open(path, O_RDONLY); if (errno != 0) { /* SAI: errno co the la rac cu */ perror("open"); } /* open co the thanh cong nhung errno van mang gia tri tu mot loi truoc do, nen nhanh nay chay nham. */
Kiểm tra giá trị trả về trước
int fd = open(path, O_RDONLY); if (fd < 0) { /* kiem tra GIA TRI TRA VE */ perror("open"); /* gio errno moi co nghia */ }

#Mã lỗi thường gặp

MãNghĩa
ENOENTTệp hoặc thư mục không tồn tại
EACCESKhông có quyền
ENOMEMHết bộ nhớ
EINVALTham số không hợp lệ
EAGAIN / EWOULDBLOCKThử lại, dùng với không chặn
EINTRBị tín hiệu ngắt, thường phải thử lại
EPIPEGhi vào ống không còn ai đọc
EEXISTTệp đã tồn tại
EMFILE / ENFILEQuá nhiều tệp mở

Tự làm thử

  1. Vẽ đường đi của printf từ user xuống write trong nhân.
  2. Trên Linux, chạy strace trên một chương trình Hello World và đếm số syscall.
  3. Viết chương trình gây mười lỗi khác nhau và in tên mã lỗi cho từng cái.
  4. Chứng minh quy tắc hai: đọc errno sau một printf xen vào và thấy nó bị đổi.
  5. Xử lý EINTR đúng cách trong một vòng lặp read.
  6. Phân biệt man 2 write và man 3 printf và tóm tắt khác biệt.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • System call là cổng duy nhất từ chương trình (ring 3) tới nhân (ring 0); nhân kiểm tra quyền trước khi làm.
  • Mỗi syscall tốn 100 tới 1000 chu kỳ, nên stdio phải đệm và đọc từng byte chậm hơn nghìn lần.
  • errno chỉ có nghĩa sau khi hàm báo lỗi; đọc ngay, lưu ngay, và nó là thread-local.
  • Kiểm tra giá trị trả về của hàm trước, rồi mới đọc errno.
  • EINTR, EAGAIN, EPIPE là ranh giới giữa lỗi thật và tình huống bình thường phải xử lý.