Hôm nay mình sẽ nói về system call một khái niệm cơ bản trong linux. System call là cách mà một chương trình giao tiếp với hệ điều hành — cụ thể là để yêu cầu kernel thực hiện một công việc nào đó, chẳng hạn như đọc hoặc ghi file, tạo tiến trình mới, hay cấp phát bộ nhớ.
Mỗi một hệ điều hành sẽ có các tập lệnh system calls khác nhau dành riêng cho hệ điều hành đó.
Trong Linux, khi một chương trình thực hiện lời gọi hệ thống, quyền điều khiển sẽ được chuyển từ chế độ người dùng (user mode) sang chế độ nhân (kernel mode) — tức là từ phần chương trình bình thường sang phần có quyền truy cập sâu hơn trong hệ thống. Cách chuyển đổi này tùy thuộc vào từng kiến trúc phần cứng.
System calls sẽ được chia ra làm 5 loại chính:
- File Management
- Process Control
- Device Manager
- Information Maintenance
- Communication
Đầu tiên hãy đi qua nhóm File Management, các system call có mục đích để thao tác với file trong hệ thống linux.
- open(): dùng để mở một tệp tin (file).
- read(): dùng để mở file ở chế độ đọc (reading mode), ta chỉ có thể đọc nội dung file mà không thể chỉnh sửa. Đặc biệt, nhiều tiến trình có thể đồng thời gọi read() trên cùng một file, vì thao tác đọc không làm thay đổi dữ liệu trong file.
- write(): dùng để mở file ở chế độ ghi (writing mode), ta có thể chỉnh sửa hoặc ghi dữ liệu mới vào file. Tuy nhiên, nhiều tiến trình không thể cùng lúc thực hiện write() trên cùng một file, vì điều đó có thể gây xung đột dữ liệu.
- close(): dùng để đóng một file đã được mở trước đó, giúp giải phóng tài nguyên mà tiến trình đang sử dụng để truy cập file đó.
Code mẫu:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h> // open()
#include <unistd.h> // read(), write(), close()
#include <string.h> // strlen()
int main() {
int fd;
char *filename = "example.txt";
// Create and open file
fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0644);
if (fd < 0) {
perror("open for write");
exit(1);
}
printf("File '%s' opened for writing (fd = %d)\n", filename, fd);
// Write data to file
char *text = "Hello from system call example!\n";
write(fd, text, strlen(text));
printf("Data written to file.\n");
// Close file
close(fd);
// Reopen file
char buffer[100];
fd = open(filename, O_RDONLY);
if (fd < 0) {
perror("open for read");
exit(1);
}
// Read data from file
int bytes_read = read(fd, buffer, sizeof(buffer) - 1);
buffer[bytes_read] = '\0';
printf("Data read from file: %s\n", buffer);
// Close file
close(fd);
return 0;
}
Giải thích code:
Line 13: Tạo và mở file O_CREAT (tạo file nếu chưa tồn tại), O_WRONLY (mở file ở chế độ ghi), O_TRUNC (nếu file tồn tại, xóa hết dữ liệu cũ), 0644 (quyền truy cập file với user đọc/ghi, group và others chỉ đọc).
Line 14-17: nếu open() thất bại (fd < 0), chương trình in lỗi và thoát.
Line 21-22: write() ghi dữ liệu từ text vào file được mở bởi fd. strlen(text) xác định số byte cần ghi.
Line 26: Giải phóng file descriptor, đảm bảo dữ liệu ghi ra disk hoàn tất.
Line 30: Mở file ở chế độ chỉ đọc (O_RDONLY).
Line 31-34: Nếu mở không thành công, chương trình in lỗi và thoát.
Line 37: Đọc dữ liệu từ file và lưu vào mảng buffer. read() đọc tối đa sizeof(buffer) - 1 byte từ file vào buffer.
Line 38: Thêm ký tự \0 để kết thúc chuỗi (vì read chỉ trả về dữ liệu thô).
Line 39: In dữ liệu vừa đọc ra màn hình.
Line 41: close(fd); Giải phóng file descriptor, kết thúc thao tác đọc.
.png)
Tiếp theo, mình sẽ cùng đi qua nhóm Process Control, nhóm này này chịu trách nhiệm thực hiện các tác vụ liên quan đến việc quản lý tiến trình, chẳng hạn như tạo tiến trình mới (process creation), kết thúc hoặc chấm dứt tiến trình (process termination), cũng như các thao tác khác như thay đổi trạng thái tiến trình, đồng bộ hóa giữa các tiến trình.
fork():
- Một tiến trình mới được tạo ra.
- Tiến trình mới này có thể được tạo ra mà không cần chạy một chương trình mới — tiến trình con (sub-process) đơn giản là tiếp tục thực thi cùng chương trình mà tiến trình cha đang chạy tại thời điểm gọi.
exit()
- Được sử dụng bởi một chương trình để kết thúc quá trình thực thi của nó.
- Sau khi exit() được gọi, hệ điều hành sẽ thu hồi lại tất cả các tài nguyên (như bộ nhớ, file descriptor, v.v.) mà tiến trình đó đã sử dụng.
exec()
- Sau khi gọi một chương trình mới sẽ bắt đầu được thực thi.
- Việc chạy một chương trình mới không yêu cầu phải tạo tiến trình mới; bất kỳ tiến trình nào cũng có thể gọi exec() vào bất kỳ thời điểm nào.
- Khi đó, chương trình hiện tại sẽ ngay lập tức bị chấm dứt, và chương trình mới bắt đầu chạy trong cùng ngữ cảnh của tiến trình hiện có.
Code mẫu:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid;
printf("Parent process started (PID = %d)\n", getpid());
pid = fork();
if (pid < 0) {
perror("fork failed");
exit(1);
}
else if (pid == 0) {
printf("Child process (PID = %d) is running...\n", getpid());
printf("Child executing 'ls -l' using exec...\n");
execl("/bin/ls", "ls", NULL);
printf("Hello to exit child process");
}
else {
printf("Parent process waiting for child (PID = %d)...\n", pid);
int status;
wait(&status);
printf("Parent process exiting.\n");
exit(30);
}
}
Giải thích code:
Line 9: chương trình khởi đầu trong user space, là tiến trình cha (parent). getpid() in ra PID của tiến trình hiện tại (PID thật của cha).
Line 11: fork() tạo một bản sao của tiến trình hiện tại. Sau khi gọi, hệ thống sẽ có hai tiến trình đang chạy cùng lúc. Giá trị trả về của fork() (tức là giá trị của biến pid như code mẫu trên) sẽ có các giá trị khác nhau: một số nguyên dương (tiến trình cha), 0 (tiến trình con), -1 (lỗi).
Line 13-33: nhánh điều kiện
if (pid < 0) { ... } // Lỗi fork
else if (pid == 0) { ... } // Tiến trình con
else { ... } // Tiến trình cha
Khi fork() thành công: Cả cha và con đều chạy cùng một code, nhưng rẽ nhánh ở phần if để thực hiện nhiệm vụ riêng.
Line 13-16: In ra PID của chính tiến trình con. Sau đó: execl() thay toàn bộ không gian tiến trình hiện tại bằng chương trình /bin/ls. Sau lệnh này, code cũ của tiến trình con (toàn bộ main) bị ghi đè bởi chương trình ls. Nghĩa là mọi dòng sau execl() sẽ không được thực thi nếu execl() thành công.
Dòng: printf("Hello to exit child process"); sẽ không bao giờ chạy (vì execl() đã thay thế tiến trình con bằng ls).
Line 17-24: Trong nhánh else, wait() khiến tiến trình cha tạm dừng (block) cho đến khi tiến trình con kết thúc. Kernel sẽ gửi lại cho cha:
- PID của con đã kết thúc.
- Trạng thái thoát (exit code) trong biến status.
Sử dụng wait() để giúp tránh process con trở thành zombie process.
Sau đó tiến trình cha thoát với mã 30. Bạn có thể kiểm tra bằng câu lệnh: echo $?


