1. Giới thiệu chung
Khái niệm file
File là instance của một đối tượng nào đó. Ví dụ process, device, data.
Trong Linux, “tất cả đều là file” là một quy tắc cơ bản. Cả dữ liệu và thư mục cũng được biểu diễn dưới dạng các tệp. Thậm chí các thiết bị như ổ đĩa, bàn phím, màn hình cũng là các tệp đặc biệt trong hệ thống file
- Mọi thông tin đều có thể được lấy qua file.
- Mọi request đều được thực hiện qua file.
Phân loại file
Regular file:
- Là cái file chứa dữ liệu và tồn tại thực tế trong ổ cứng
- Không bị mất đi khi khởi động lại hệ thống
- Mỗi loại file sẽ có format dữ liệu riêng
Directory file:
- Là file nằm trên ổ cứng.
- Dữ liệu trong thư mục là tên là một số thông tin của các file nằm trong nó.
Các loại file khác:
- Meta data: Ví dụ khi dùng Makefile build code thì sẽ sinh ra 1 loạt file ẩn để chứa thông tin lần build đó.
- Symbolic link
- Virtual file
- Socket: Được lưu trong /proc/net/tcp
- Device file: Được lưu trong /dev
Các virtual file không được lưu trên ổ cứng mà sẽ được OS khởi tạo và nằm trong RAM. Khi tắt máy chúng sẽ biến mất và được tạo lại khi OS khởi động
File trong Linux được liên kết với nhau theo sơ đồ cây, còn được gọi là cây thư mục:
![](https://vinalinux.com.vn/wp-content/uploads/2023/11/image.png)
Quá trình khởi tạo file system
Khi hệ điều hành bắt đầu quá trình boot, hệ điều hành đọc dữ liệu từ ổ cứng tạo ra các cây thư mục liên quan tới regular file. Ngoài ra, các file ảo như trong thư mục /proc/pid ( chứa các process ) hoặc trong thư mục /dev/ ( thông tin về các phần cứng trong hệ thống)… sẽ được hệ điều hành tự động tạo ra
![](https://vinalinux.com.vn/wp-content/uploads/2023/11/image-1.png)
Hệ thống thư mục của file system trên Linux:
- /bin Các chương trình cơ bản
- /boot Chứa nhân Linux để khởi động và các file system maps cũng như các file khởi động giai đoạn hai.
- /dev Chứa các tập tin thiết bị (CDRom, HDD, FDD….).
- /etc Chứa các tập tin cầu hình hệ thống.
- /home Thư mục dành cho người dùng khác root.
- /lib Chứa các thư viện dùng chung cho các lệnh nằm trong /bin và /sbin. Và thư mục này cũng chứa các module của kernel.
- /mnt hoặc /media Mount point mặc định cho những hệ thống file kết nối bên ngoài.
- /opt Thư mục chứa các phần mềm cài thêm.
- /sbin Các chương trình hệ thống
- /srv Dữ liệu được sử dụng bởi các máy chủ lưu trữ trên hệ thống.
- /tmp Thư mục chứa các file tạm thời.
- /usr Thư mục chứa những file cố định hoặc quan trọng để phục vụ tất cả người dùng.
- /var Dữ liệu biến được xử lý bởi daemon. Điều này bao gồm các tệp nhật ký, hàng đợi, bộ đệm, bộ nhớ cache,…
- /root Các tệp cá nhân của người quản trị (root)
- /proc Sử dụng cho Linux kernel. Chúng được sử dụng bởi nhân để xuất dữ liệu sang không gian người dùng.
Quản trị file trên Linux
Khi ta liệt kê các file bằng câu lệnh ls -l, hệ thống sẽ hiển thị thông tin chi tiết về các file:
![](https://vinalinux.com.vn/wp-content/uploads/2023/11/image-3.png)
Loại file và quyền của file được đại diện bởi 10 kí tự:
![](https://vinalinux.com.vn/wp-content/uploads/2023/11/image-4.png)
Kí tự đầu tiên:
- d : thư mục
- – : regular file
9 kí tự tiếp theo đại diện cho các quyền của file ( 3 bit 8-7-6 dành cho user, 3 bit 5-4-3 dành cho group, 3 bit 2-1-0 dành cho other)
- user permission r-w-x
- group permission r-w-x
- others permission r-w-x
Command line thay đổi các quyền trên của file:
1 |
chmod [quyền] [tệp/thư mục] |
Lệnh chmod (Change Mode) được sử dụng trong hệ điều hành Linux và Unix để thay đổi quyền truy cập của tệp và thư mục. Lệnh này cho phép bạn cấu hình quyền đọc (read), ghi (write) và thực thi (execute) cho user, group và others.
- [quyền]: là cách bạn xác định quyền truy cập mới cho tệp/thư mục, thường là một chuỗi ký tự được biểu diễn dưới dạng số nhị phân (ví dụ: “755”) hoặc bằng cách sử dụng ký hiệu thay vì số (ví dụ: “u+rwx,go=rx”)
- [tệp/thư mục]: là tên của tệp hoặc thư mục mà bạn muốn thay đổi quyền truy cập.
Các ký hiệu quyền:
- u đại diện cho chủ sở hữu (user).
- g đại diện cho nhóm (group).
- o đại diện cho người dùng khác (others).
Các ký hiệu quyền:
- + để thêm quyền truy cập.
- – để loại bỏ quyền truy cập.
- = để thiết lập quyền truy cập.
Các quyền truy cập:
- r đại diện cho quyền đọc (read).
- w đại diện cho quyền ghi (write).
- x đại diện cho quyền thực thi (execute).
Ví dụ sử dụng lệnh chmod:
Để cấp quyền đọc và ghi cho chủ sở hữu của tệp “data”
1 |
chmod u+rw data |
Để thiết lập quyền đọc và thực thi cho chủ user và group của tệp “hello”, và chỉ cấp quyền đọc cho others:
1 |
chmod u+rx,g+rx,o-r hello |
1 |
chmod 664 hello |
2. Các hàm cơ bản tương tác với file
Kernel cung cấp một bộ system call cơ bản để thực hiện việc đọc ghi và thao tác với file, bao gồm các hàm sau:
- int open(const char *pathname, int flags).
- int close(int fd).
- ssize_t read(int fd, void *buf, size_t count).
- ssize_t write(int fd, const void *buf, size_t count)
- off_t lseek(int fd, off_t offset, int whence)
- void sync(void): Hàm này được sử dụng để đồng bộ hóa dữ liệu trong bộ nhớ đệm với thiết bị lưu trữ trên hệ thống tệp. Nó đảm bảo rằng tất cả dữ liệu đang ở trong bộ nhớ đệm đã được ghi vào thiết bị lưu trữ.
Mở một file: open()
System call open() sẽ mở một tệp hiện có hoặc tạo và mở một tệp mới.
1 2 3 4 |
#include <sys/stat.h> #include <fcntl.h> int open(const char *pathname, int flags, ... /* mode_t mode */); |
- pathname: Là một con trỏ đến một chuỗi ký tự chứa đường dẫn đến tệp cần mở hoặc tạo. Đường dẫn có thể là tuyệt đối hoặc tương đối.
- flags: Là một số nguyên biểu thị các cờ và quyền mở. Các cờ này có thể được kết hợp bằng toán tử OR (|). Một số cờ phổ biến bao gồm:
- O_RDONLY: Mở tệp chỉ để đọc.
- O_WRONLY: Mở tệp chỉ để ghi.
- O_RDWR: Mở tệp để đọc và ghi.
- O_CREAT: Tạo tệp nếu nó không tồn tại.
- O_APPEND: Ghi thêm vào cuối tệp (không ghi đè dữ liệu).
- O_TRUNC: Xoá nội dung của tệp nếu nó tồn tại.
- mode: Chỉ định quyền truy cập cho file mới được tạo nếu O_CREAT được sử dụng. Chỉ có giá trị này được sử dụng khi bạn tạo một tệp mới. Bạn thường sử dụng các hằng số như S_IRUSR, S_IWUSR, S_IRGRP, và S_IWGRP để xác định quyền cho file mới tạo.
Hàm open() trả về một số nguyên, là một file descriptor mới. File descriptor này là một nguyên số không âm. Nếu việc mở tệp thất bại, trả về giá trị -1
Chi tiết về hàm open(): open(2) – Linux manual page (man7.org)
Ví dụ việc sử dụng hàm open():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/* Open existing file for reading */ fd = open("startup", O_RDONLY); if (fd == -1) errExit("open"); /* Open new or existing file for reading and writing, truncating to zero bytes; file permissions read+write for owner, nothing for all others */ fd = open("myfile", O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR); if (fd == -1) errExit("open"); /* Open new or existing file for writing; writes should always append to end of file */ fd = open("w.log", O_WRONLY | O_CREAT | O_TRUNC | O_APPEND, S_IRUSR | S_IWUSR); if (fd == -1) errExit("open"); |
Hàm đọc từ file: read()
System call read() được sử dụng để đọc dữ liệu từ file đã mở tham chiếu bởi file descriptor fd vào một buffer( bộ đệm).
1 2 3 |
#include <unistd.h> ssize_t read(int fd, void *buffer, size_t count); |
- fd: Là file descriptor của tệp bạn muốn đọc. Điều này có nghĩa là bạn cần phải mở tệp bằng cách sử dụng hàm open() trước đó và sau đó sử dụng file descriptor của tệp mở đó.
- buffer: Là một con trỏ đến bộ đệm (buffer) mà dữ liệu đọc sẽ được sao chép vào.
- count: Là số lượng byte bạn muốn đọc từ tệp.
Hàm read() trả về số lượng byte thực sự đã đọc từ tệp, hoặc -1 nếu có lỗi. Nếu nó trả về 0, nghĩa là bạn đã đọc đến cuối tệp
Chú ý: System call không cấp phát bộ nhớ cho bộ đệm được sử dụng để trả về thông tin cho người dùng. Thay vào đó, chúng ta phải chuyển một con trỏ tới bộ nhớ đệm được phân bổ bộ nhớ có kích thước chính xác trước đó.
Ví dụ về việc sử dụng hàm read():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <stdio.h> #include <fcntl.h> #include <unistd.h> int main() { int fd; char buffer[1024]; ssize_t bytes_read; fd = open("example.txt", O_RDONLY); if (fd == -1) { return 1; } bytes_read = read(fd, buffer, sizeof(buffer)); if (bytes_read == -1) { close(fd); return 1; } // Xử lý dữ liệu trong buffer close(fd); return 0; } |
Trong ví dụ trên, chúng ta sử dụng hàm read() để đọc dữ liệu từ tệp “example.txt” đã mở và sau đó dữ liệu được sao chép vào bộ đệm buffer. Hàm read() trả về số lượng byte đã đọc, và sau đó chúng ta có thể xử lý dữ liệu trong buffer.
Ghi vào file: write()
System call write() được sử dụng để ghi dữ liệu từ buffer vào trong một file đang được mở.
1 2 3 |
#include <unistd.h> ssize_t write(int fd, void *buffer, size_t count); |
- fd: Là file descriptor của tệp bạn muốn ghi dữ liệu vào. Điều này có nghĩa là bạn cần phải mở tệp bằng cách sử dụng hàm open() trước đó và sau đó sử dụng file descriptor của tệp mở đó.
- buffer: Là một con trỏ đến bộ đệm (buffer) chứa dữ liệu bạn muốn ghi vào tệp.
- count: Là số lượng byte bạn muốn ghi vào tệp từ bộ đệm
Hàm write() trả về số lượng byte thực sự đã ghi vào tệp, hoặc -1 nếu có lỗi. Đôi khi số lượng byte ghi được có thể ít hơn số lượng byte người dùng muốn ghi vào.
Ví dụ về việc sử dụng hàm write():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <stdio.h> #include <fcntl.h> #include <unistd.h> int main() { int fd; char buffer[] = "Hello, World!"; ssize_t bytes_written; fd = open("output.txt", O_WRONLY | O_CREAT, 0644); if (fd == -1) { return 1; } bytes_written = write(fd, buffer, sizeof(buffer)); if (bytes_written == -1) { close(fd); return 1; } close(fd); return 0; } |
Trong ví dụ trên, chúng ta sử dụng hàm write() để ghi dữ liệu từ bộ đệm buffer vào tệp “output.txt” đã mở. Hàm write() trả về số lượng byte đã ghi và sau đó chúng ta đóng tệp sử dụng hàm close()
Đóng file đang mở: close()
System call close() sẽ đóng một file desciptor đang mở, giải phóng nó trong bảng file table để process sử dụng lại sau này. Khi một process kết thúc, tất cả các file desciptor đang mở của nó sẽ tự động đóng lại.
1 2 3 |
#include <unistd.h> int close(int fd); |
Hàm close() trả về 0 nếu nó đóng thành công file descriptor. Nếu có lỗi trong quá trình đóng, nó trả về -1.
Chú ý: file descriptors là một tài nguyên hữu hạn, do đó, việc không đóng file descriptors có thể dẫn đến process hết descriptors. Đây là một vấn đề đặc biệt quan trọng khi viết các chương trình tồn tại lâu dài xử lý nhiều tệp, chẳng hạn như shell hoặc networks sever.
Thay đổi vị trí con trỏ: lseek()
Đối với mỗi tệp đang mở, kernel ghi lại một offset của tệp (con trỏ đọc-ghi). Đây là vị trí trong tệp mà tại đó quá trình read() hoặc write() tiếp theo sẽ bắt đầu.
Offset của tệp được đặt để trỏ đến phần đầu của tệp khi tệp được mở và được tự động điều chỉnh bởi mỗi lệnh read() hoặc write() để nó trỏ đến byte cần đọc hoặc viết. Do đó, các lệnh gọi read() và write() liên tiếp sẽ tiến triển tuần tự thông qua một tệp.
System call lseek() được sử dụng để di chuyển vị trí con trỏ trong tệp đã mở được tham chiếu bởi file descriptor (fd). Hàm này cho phép bạn thực hiện việc đọc hoặc ghi dữ liệu từ hoặc đến một vị trí cụ thể trong tệp.
1 2 |
#include <unistd.h> off_t lseek(int fd, off_t offset, int whence); |
- fd: Là file descriptor của tệp bạn muốn thay đổi vị trí con trỏ.
- offset: Là số lượng byte bạn muốn di chuyển con trỏ đến. Offset có thể là một giá trị dương hoặc âm, và nó xác định sự thay đổi vị trí hiện tại của con trỏ.
- whence: Là một trong ba giá trị sau đây:
- SEEK_SET: Đặt vị trí con trỏ từ đầu tệp.
- SEEK_CUR: Đặt vị trí con trỏ từ vị trí hiện tại.
- SEEK_END: Đặt vị trí con trỏ từ cuối tệp.
![](https://vinalinux.com.vn/wp-content/uploads/2023/11/image-5.png)
Hàm lseek() trả về vị trí mới của con trỏ sau khi di chuyển thành công, hoặc -1 nếu có lỗi.
3. Quản lý file trong Linux
Trong hệ thống tệp của Linux, quản lý tệp được thực hiện thông qua các thành phần quan trọng bao gồm file descriptor, open file table và inode table.
File Descriptor:
- Mỗi tiến trình khi thực thi có một bảng file descriptor riêng, gọi là bảng file descriptor.
- File descriptor là một số nguyên không âm, đại diện cho một tệp đã mở hoặc một luồng dữ liệu khác.
- Khi một tệp được mở, hệ thống gán một số file descriptor cho nó và lưu trữ thông tin về tệp trong bảng file descriptor của tiến trình.
- Các hoạt động trên tệp (đọc, ghi, đóng) được thực hiện thông qua file descriptor.
Open File Table:
- Mỗi file descriptor trong tiến trình trỏ đến một mục trong bảng tệp đã mở.
- Bảng tệp đã mở (open file table) chứa thông tin về các tệp đã mở, bao gồm file position (vị trí con trỏ), quyền truy cập, trạng thái tệp (đã mở, đang đọc, đang ghi), vv.
Inode Table:
- Inode là một cấu trúc dữ liệu quan trọng trong hệ thống tệp Linux, nó chứa thông tin về tệp, chẳng hạn như quyền truy cập, kích thước, địa chỉ vùng dữ liệu, vv.
- Mỗi tệp trong hệ thống tệp Linux có một inode tương ứng.
- Khi một tệp được mở, hệ thống sẽ sử dụng inode để kiểm tra quyền truy cập và cập nhật thông tin về tệp trong Open file table.
- Inode cũng chứa định danh duy nhất cho tệp (inode number)
Quá trình quản lý tệp trong Linux thông qua các thành phần trên diễn ra như sau:
- Khi một tệp được mở, hệ thống tạo một Open file table entry và một File descriptor cho tệp đó trong bảng File descriptor của tiến trình.
- Open file table entry sẽ trỏ đến inode tương ứng với tệp đã mở.
- Mọi hoạt động trên tệp (đọc, ghi) sẽ được thực hiện thông qua file descriptor và sẽ được ghi vào Open file table.
- Khi tệp được đóng, thông tin về tệp trong Open file table sẽ được cập nhật, và file descriptor sẽ được giải phóng
Ví dụ mối quan hệ giữa file descriptor table, open file table và inode table:
![](https://vinalinux.com.vn/wp-content/uploads/2023/11/image-6.png)
Trong quá trình thực hiện của tiến trình A, có hai file descriptors fd1 và fd20 cùng trỏ đến một mô tả tệp mở (open file description) duy nhất, được đánh dấu là 23.
File descriptor fd2 của tiến trình A và file descriptor fd2 của tiến trình B đều trỏ đến cùng một mô tả tệp trong open file description, được đánh dấu là 73. Tình huống này có thể xảy ra sau một cuộc gọi đến hàm fork() (nghĩa là tiến trình A là tiến trình cha của tiến trình B hoặc ngược lại).
Cuối cùng, chúng ta thấy số file descriptor fd0 của tiến trình A và fd3 của tiến trình B trỏ đến các vị trí trong Open file table khác nhau, nhưng chúng tham chiếu đến cùng một bảng inode (i-node table entry), được đánh dấu là 1976 – nghĩa là tham chiếu đến cùng một tệp. Tình huống này xảy ra khi mỗi tiến trình độc lập gọi hàm open() cho cùng một tệp. Tình huống tương tự có thể xảy ra nếu một tiến trình mở cùng một tệp hai lần.
4. Cached data của file
Hệ thống sử dụng 1 phần RAM làm bộ nhớ cached cho việc đọc ghi file:
- Việc đọc ghi dữ liệu trong file thông thường sẽ đọc qua cached để tăng tốc độ của hệ thống. Ví dụ như việc đọc 1 byte từ ổ cứng, khi đó OS vẫn đọc cả sector là 512 bytes, tuy nhiên chỉ lấy 1 bytes trả về cho app, số bytes còn lại được cất vào cached nằm trong RAM, nếu lần đọc sau đọc data nằm trong sector đó thì sẽ lấy từ cached mà không cần đọc xuống ổ cứng.
- Việc ghi data thông thường OS sẽ ghi vào cached nằm trong RAM. Trong kernel có 1 thread sẽ định kỳ flush tất cả các cached và file. Ngoài ra có thể sử dụng hàm sync() hoặc setting flag lúc open file để chỉ định không sử dụng cached
![](https://vinalinux.com.vn/wp-content/uploads/2023/11/image-7.png)
Mỗi file khi open sẽ tạo ra 1 inode. Mỗi inode sẽ có trỏ đến vùng nhớ cached riêng của nó
Bộ nhớ cached của 1 file có thể được flush theo cách chủ động hoặc bị động:
- Chủ động flush cached: gọi hàm fflush, sync(), fsync() hoặc close() file
- Bị động flush cached: Process kết thúc bằng hàm exit hoặc câu lệnh return hoặc được kernel thread flush cached