Mục lục khóa họcMở

Chương 1 · Bài 1Học thử miễn phí

Linux File System Overview

Mục chấm điểmLàm thử

Làm thử miễn phí ngay trên web, không cần đăng nhập, không tính điểm.

Bài này là phần đầu của loạt hai bài về file system trên Linux và chỉ nói về khái niệm: Linux coi những gì là file, các file được tổ chức thành cây thư mục như thế nào, file được lưu trên đĩa ra sao thông qua inode và hai loại liên kết, và vì sao cùng một giao diện open()/read()/write() dùng được cho cả đĩa, RAM lẫn thiết bị. Cách đọc và ghi file bằng system call được trình bày ở bài File Operations. Mọi ví dụ đều được biên dịch và chạy trực tiếp trên Linux.

Mục tiêu bài học

Sau bài học này, bạn sẽ có thể:

  • Phân biệt các loại file trên Linux và đọc thuộc tính của file bằng lstat().
  • Mô tả cây thư mục bắt đầu từ / và vai trò của các thư mục chuẩn.
  • Giải thích inode, thư mục và sự khác nhau giữa hard link với symbolic link.
  • Mô tả vai trò của VFS và nhận biết các loại filesystem đang được mount.

1. Mọi thứ đều là file

Triết lý của UNIX là "everything is a file": đĩa cứng, bàn phím, terminal, pipe, socket và cả thông tin của kernel đều được truy cập qua cùng một giao diện file. Nhờ vậy lệnh cat, echo hay hàm read(), write() dùng được với rất nhiều đối tượng khác nhau mà không cần API riêng cho từng loại.

1.1. Các loại file

Mỗi file có một kiểu (type) lưu trong thuộc tính của nó. Ký tự đầu tiên trong kết quả của ls -l cho biết kiểu đó:

Ký tựKiểuÝ nghĩaMacro kiểm tra
-Regular fileFile dữ liệu thông thường: văn bản, ảnh, chương trình.S_ISREG()
dDirectoryThư mục, chứa danh sách tên file.S_ISDIR()
lSymbolic linkFile lưu đường dẫn trỏ tới file khác.S_ISLNK()
cCharacter deviceThiết bị truy cập theo từng byte (terminal, /dev/null).S_ISCHR()
bBlock deviceThiết bị truy cập theo khối (ổ đĩa, phân vùng).S_ISBLK()
pFIFO (named pipe)Kênh truyền dữ liệu giữa các process.S_ISFIFO()
sSocketĐiểm cuối giao tiếp mạng hoặc giữa các process.S_ISSOCK()

Thông tin của một file (kiểu, kích thước, quyền, số inode...) được kernel trả về qua họ hàm stat(). Hàm lstat() giống stat() nhưng không đi theo symbolic link, nó mô tả chính link đó:

#include <sys/stat.h>

/**
 * @brief Get the metadata of a file without following a symbolic link.
 *
 * @param path Path of the file.
 * @param buf  Receives the metadata (type and mode in st_mode, inode in st_ino,
 *             link count in st_nlink, size in st_size).
 * @return 0 on success, -1 on error (errno is set).
 */
int lstat(const char *path, struct stat *buf);

1.1.1. Ví dụ 1

Chương trình sau in kiểu, số inode, số link, kích thước và quyền của từng đường dẫn nhận từ dòng lệnh. File examples/file_info.c:

/**
 * @file file_info.c
 * @brief Print the type, inode, link count, size and mode of each path with lstat().
 */

#include <stdio.h>
#include <sys/stat.h>

/**
 * @brief Name the file type stored in st_mode.
 * @param m The st_mode value.
 * @return Type name.
 */
static const char *type(mode_t m)
{
    if (S_ISREG(m))
        return "regular";
    if (S_ISDIR(m))
        return "directory";
    if (S_ISLNK(m))
        return "symlink";
    if (S_ISCHR(m))
        return "char device";
    if (S_ISBLK(m))
        return "block device";
    if (S_ISFIFO(m))
        return "fifo";
    return "socket";
}

/**
 * @brief Show the metadata of every path given on the command line.
 * @param argc Number of arguments, including the program name.
 * @param argv Program name followed by the paths.
 * @return 0 on success, 1 when a path cannot be read.
 */
int main(int argc, char *argv[])
{
    struct stat st; /**< Metadata filled by lstat(). */

    for (int i = 1; i < argc; i++) {
        if (lstat(argv[i], &st)) {
            perror(argv[i]);
            return 1;
        }
        printf("%s: type=%s inode=%lu links=%lu size=%ld mode=%04o\n", argv[i],
               type(st.st_mode), (unsigned long)st.st_ino,
               (unsigned long)st.st_nlink, (long)st.st_size, st.st_mode & 07777);
    }
    return 0;
}

Chạy make trong thư mục của bài để build tất cả ví dụ, các file chạy được nằm trong thư mục build/:

$ make
mkdir -p build
gcc -Wall examples/file_info.c -o build/file_info 
gcc -Wall examples/link_demo.c -o build/link_demo 

Tạo một file thông thường rồi cho chương trình xem nó cùng với vài loại file khác:

$ echo hello > notes.txt
$ ./build/file_info notes.txt . /dev/null /dev/sda /bin/sh /tmp
notes.txt: type=regular inode=20889140 links=1 size=6 mode=0664
.: type=directory inode=20888809 links=5 size=4096 mode=0775
/dev/null: type=char device inode=6 links=1 size=0 mode=0666
/dev/sda: type=block device inode=356 links=1 size=0 mode=0660
/bin/sh: type=symlink inode=1835120 links=1 size=4 mode=0777
/tmp: type=directory inode=29097985 links=128 size=12288 mode=1777

Số inode, số link và kích thước phụ thuộc vào máy bạn nên sẽ khác ở các dòng trên, nhưng các quan hệ sau luôn đúng:

  • notes.txt có size=6 vì echo ghi 5 ký tự hello cộng ký tự xuống dòng.
  • Thư mục . có links=5: mỗi thư mục có 2 link (tên của nó trong thư mục cha và mục . bên trong) cộng thêm 1 link .. từ mỗi thư mục con. Thư mục của bài có 3 thư mục con (build, examples, images) nên 2 + 3 = 5.
  • /dev/null và /dev/sda có size=0 vì file thiết bị không chứa dữ liệu trên đĩa, nó chỉ là "cánh cửa" vào driver trong kernel.
  • /bin/sh là symbolic link có size=4: kích thước chính là độ dài đường dẫn dash mà link lưu.
  • /tmp có mode=1777: số 1 đầu tiên là sticky bit, cho phép mọi người tạo file trong thư mục nhưng chỉ chủ của file mới được xóa file đó.

1.2. Device file và pseudo file

Các file thiết bị nằm trong thư mục /dev. Kết quả của ls -l cho một file thiết bị có hai số đặt thay cho kích thước:

$ ls -l /dev/null /dev/sda /dev/tty
crw-rw-rw- 1 root root 1, 3 Oct 28 11:50 /dev/null
brw-rw---- 1 root disk 8, 0 Oct 28 11:50 /dev/sda
crw-rw-rw- 1 root tty  5, 0 Nov  2 22:33 /dev/tty

Hai số 8, 0 là major và minor: major chọn driver trong kernel (8 là driver ổ đĩa SCSI/SATA), minor chọn thiết bị cụ thể do driver đó quản lý. Khi chương trình read() hay write() vào file thiết bị, kernel chuyển lời gọi cho driver tương ứng thay vì đọc ghi đĩa.

Linux còn có các file "ảo" không tồn tại trên đĩa. Ví dụ file /proc/loadavg được kernel tạo ra ngay lúc bạn đọc, và cat đọc nó như một file bình thường:

$ cat /proc/loadavg
1.28 0.43 0.21 1/1726 412083

Ba số đầu là tải trung bình của hệ thống trong 1, 5 và 15 phút. Các thư mục /proc và /sys là cách kernel công bố thông tin ra user space bằng giao diện file.

2. Cây thư mục

Linux không có khái niệm ổ đĩa C:, D:. Mọi file thuộc về một cây thư mục duy nhất bắt đầu từ thư mục gốc / (root directory). Các thiết bị lưu trữ khác được gắn (mount) vào một thư mục nào đó trong cây này.

2.1. Thư mục gốc và các thư mục chuẩn

Cách đặt tên và công dụng của các thư mục ngay dưới / được chuẩn hóa bởi FHS (Filesystem Hierarchy Standard), nên các bản phân phối Linux có bố cục tương tự nhau. Hình dưới đây là các thư mục thường gặp nhất:

linux_file_system_overview-hinh-01
Thư mụcNội dung
/bin, /sbinLệnh cơ bản cho người dùng và cho quản trị hệ thống (ls, cp, mount).
/devFile thiết bị (xem mục 1.2).
/etcFile cấu hình của hệ thống.
/homeThư mục riêng của từng người dùng (/home/aosp).
/proc, /sysFile ảo do kernel tạo ra, mô tả process và phần cứng.
/usrChương trình, thư viện và tài liệu cài đặt thêm (/usr/bin, /usr/lib).
/varDữ liệu thay đổi trong lúc chạy: log, hàng đợi, cache.
/tmpFile tạm, ai cũng ghi được (sticky bit như ví dụ trên).

Hai thư mục màu cam trong hình là /dev và /proc: nội dung của chúng không được lưu trên đĩa mà do kernel sinh ra khi truy cập.

2.2. Đường dẫn

Một file được xác định bằng đường dẫn (path) gồm các tên thư mục ngăn cách bởi /:

  • Đường dẫn tuyệt đối bắt đầu từ /, ví dụ /etc/hostname, có nghĩa như nhau ở mọi nơi.
  • Đường dẫn tương đối không bắt đầu bằng /, được hiểu từ thư mục làm việc hiện tại (current working directory) của process, ví dụ examples/file_info.c.
  • Mỗi thư mục luôn có hai mục đặc biệt: . là chính thư mục đó và .. là thư mục cha. Riêng với / thì .. cũng là chính nó.
  • Tên file phân biệt chữ hoa, chữ thường và có thể chứa mọi ký tự trừ / và byte NUL. Tên không có phần mở rộng bắt buộc: đuôi .c hay .txt chỉ là quy ước của người dùng.

3. Inode và thư mục

Phần này trả lời câu hỏi: file được lưu trên đĩa như thế nào và "tên file" nằm ở đâu.

3.1. Inode

Mỗi file trong filesystem (kiểu ext4 chẳng hạn) được đại diện bởi một inode (index node), là một cấu trúc nhỏ trên đĩa. Inode chứa mọi thông tin về file ngoại trừ tên:

  • Kiểu file và quyền truy cập (st_mode).
  • Chủ sở hữu (uid) và nhóm (gid).
  • Kích thước, thời gian truy cập, sửa đổi và thay đổi trạng thái.
  • Số link (link count): có bao nhiêu tên đang trỏ tới inode này.
  • Con trỏ tới các khối dữ liệu (data block) chứa nội dung file.

Mỗi inode có một số duy nhất trong filesystem gọi là số inode (st_ino). Còn thư mục thực chất cũng là một file, nội dung của nó là bảng ánh xạ từ tên file sang số inode. Hình dưới đây mô tả mối quan hệ này (số inode trong hình chỉ để minh họa):

linux_file_system_overview-hinh-02
  1. Khi mở notes.txt, kernel đọc thư mục chứa nó để tìm số inode ứng với tên notes.txt.
  2. Kernel đọc inode đó để kiểm tra quyền và lấy vị trí các khối dữ liệu.
  3. Nội dung file được đọc từ các khối dữ liệu này.

Vì tên nằm trong thư mục chứ không nằm trong inode nên một inode có thể có nhiều tên, và đổi tên hoặc di chuyển file trong cùng filesystem chỉ là sửa bảng của thư mục, không chép lại dữ liệu.

3.2. Hard link và symbolic link

Có hai cách để một file có thêm tên khác:

  • Hard link (link(), lệnh ln) thêm một mục tên mới vào thư mục, mục này trỏ tới cùng inode. Sau đó không còn tên nào là "bản gốc", số link của inode tăng lên. Hard link chỉ dùng trong cùng một filesystem và không dùng cho thư mục.
  • Symbolic link (symlink(), lệnh ln -s) tạo ra một file mới có inode riêng, nội dung của nó là một đường dẫn. Khi mở symbolic link, kernel đọc đường dẫn đó rồi mở file ở đường dẫn ấy. Symbolic link có thể trỏ tới thư mục, sang filesystem khác, hoặc trỏ tới nơi không tồn tại.
linux_file_system_overview-hinh-03

File sẽ thực sự bị xóa khỏi đĩa khi số link giảm về 0 và không còn process nào đang mở nó. Lệnh rm (system call unlink()) chỉ xóa một tên và giảm số link đi 1.

3.2.1. Ví dụ 2

Chương trình sau nhận một file có sẵn, tạo hard link hard.txt và symbolic link soft.txt tới nó, rồi in số inode và số link của cả ba tên. File examples/link_demo.c:

/**
 * @file link_demo.c
 * @brief Create a hard link and a symbolic link to a file, then compare their inodes.
 */

#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>

/**
 * @brief Print the inode and link count that lstat() reports for a path.
 * @param path Path to inspect.
 */
static void show(const char *path)
{
    struct stat st; /**< Metadata of path. */

    lstat(path, &st);
    printf("%-9s inode=%lu links=%lu\n", path, (unsigned long)st.st_ino,
           (unsigned long)st.st_nlink);
}

/**
 * @brief Link argv[1] as hard.txt and soft.txt, then show all three names.
 * @param argc Number of arguments, including the program name.
 * @param argv Program name and the existing target file.
 * @return 0 on success, 1 on a usage or link error.
 */
int main(int argc, char *argv[])
{
    if (argc != 2) {
        printf("Usage: %s <file>\n", argv[0]);
        return 1;
    }
    unlink("hard.txt");
    unlink("soft.txt");
    if (link(argv[1], "hard.txt") || symlink(argv[1], "soft.txt")) {
        perror("link");
        return 1;
    }
    show(argv[1]);
    show("hard.txt");
    show("soft.txt");
    return 0;
}

Chạy chương trình với file notes.txt đã tạo ở ví dụ trước, rồi so sánh với ls -li (tùy chọn -i in thêm số inode ở đầu dòng):

$ ./build/link_demo notes.txt
notes.txt inode=20889140 links=2
hard.txt  inode=20889140 links=2
soft.txt  inode=20889141 links=1
$ ls -li notes.txt hard.txt soft.txt
20889140 -rw-rw-r-- 2 aosp aosp 6 Nov  7 15:54 hard.txt
20889140 -rw-rw-r-- 2 aosp aosp 6 Nov  7 15:54 notes.txt
20889141 lrwxrwxrwx 1 aosp aosp 9 Nov  7 15:54 soft.txt -> notes.txt
  • notes.txt và hard.txt có cùng số inode và links=2: đây là hai tên của một file duy nhất.
  • soft.txt có số inode khác, kiểu l, và size=9 đúng bằng độ dài chuỗi notes.txt mà nó lưu.

Sự khác nhau thể hiện rõ khi xóa tên gốc notes.txt:

$ rm notes.txt
$ cat hard.txt
hello
$ cat soft.txt
cat: soft.txt: No such file or directory

Dữ liệu vẫn còn qua hard.txt vì inode vẫn còn một link. Còn soft.txt trở thành dangling link (link treo): nó vẫn lưu đường dẫn notes.txt nhưng đường dẫn đó không còn nên cat báo lỗi. Dọn dẹp bằng rm hard.txt soft.txt.

3.3. Quyền truy cập

Trường mode trong inode gồm 9 bit quyền cho ba nhóm: chủ file (user), nhóm của file (group) và những người còn lại (other). Mỗi nhóm có ba quyền r (đọc), w (ghi), x (thực thi), viết gọn thành số bát phân:

  • 0664 là rw-rw-r--: chủ và nhóm được đọc, ghi; người khác chỉ được đọc (kết quả của notes.txt ở ví dụ 1).
  • 0777 là rwxrwxrwx: mọi quyền cho mọi người, luôn là mode của symbolic link vì quyền thật được kiểm tra trên file đích.
  • Với thư mục, r cho phép liệt kê tên, w cho phép tạo hoặc xóa file bên trong, còn x cho phép đi qua thư mục để truy cập file con.

Kernel dùng các bit này mỗi khi một process mở file. Nếu không đủ quyền, open() thất bại với lỗi EACCES (xem bài File Operations, mục 1.2).

4. VFS và các filesystem

Ổ cứng dùng ext4, thư mục /dev/shm nằm trong RAM, còn /proc do kernel sinh ra, nhưng chương trình vẫn dùng chung một cách để mở và đọc chúng. Điều này nhờ lớp VFS (Virtual File System) trong kernel. VFS định nghĩa giao diện chung (open, read, write, close...) và chuyển mỗi lời gọi tới driver của filesystem phụ trách file đó.

linux_file_system_overview-hinh-04

Mỗi filesystem được mount vào một thư mục của cây, thư mục đó gọi là mount point. Lệnh df -T cho biết loại filesystem và nơi nó được mount:

$ df -T / /dev/shm /proc
Filesystem     Type  1K-blocks      Used Available Use% Mounted on
/dev/sda1      ext4  618148864 298212352 289511672  51% /
tmpfs          tmpfs   9194696       420   9194276   1% /dev/shm
proc           proc          0         0         0    - /proc
  • / nằm trên phân vùng /dev/sda1 định dạng ext4, dữ liệu được lưu trên đĩa.
  • /dev/shm là tmpfs, dữ liệu nằm trong RAM và mất khi tắt máy hoặc unmount.
  • /proc là proc, cột dung lượng bằng 0 vì không có dữ liệu thật nào được lưu.

Hệ quả là các khái niệm ở mục 3 (inode, hard link) áp dụng cho từng filesystem riêng. Ví dụ không thể tạo hard link giữa / và /dev/shm vì hai nơi này thuộc hai filesystem khác nhau, mỗi bên có bảng inode riêng.

5. Kết luận

  • Linux coi thiết bị, pipe, socket và thông tin của kernel đều là file, nên dùng chung giao diện đọc ghi; kiểu file đọc được bằng lstat().
  • Tất cả file nằm trong một cây thư mục duy nhất bắt đầu từ /, bố cục các thư mục chuẩn theo FHS.
  • Inode lưu thuộc tính và vị trí dữ liệu của file nhưng không lưu tên, còn thư mục là bảng ánh xạ tên sang số inode.
  • Hard link là thêm tên cho cùng inode, symbolic link là file riêng lưu đường dẫn nên có thể bị treo.
  • VFS cho phép ext4, tmpfs, procfs... cùng được truy cập bằng một giao diện và được mount vào cây thư mục chung.
Học thử tiếp: Bài 3 – Linux Process Overview