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

Level 4: Mini Shell

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

  • Tách token xử lý dấu nháy và thoát ký tự
  • Nối nhiều tầng pipe và chuyển hướng vào ra
  • Chạy nền, quản lý job, và bắt tín hiệu đúng
  • Không rò rỉ mô tả tệp, dọn zombie bằng SIGCHLD

Mini Shell là dự án đầu tiên nói chuyện trực tiếp với hệ điều hành: fork, exec, pipe, dup2, và tín hiệu. Nó gói toàn bộ Phần 16 vào một chương trình mà bạn dùng nó để chạy các chương trình khác.

#Mục tiêu

terminal
./mysh
mysh$ ls -l | grep ".c" | wc -l
7
mysh$ cat data.txt > output.txt
mysh$ sleep 30 &
[1] 4821
mysh$ jobs
[1]  Running   sleep 30
mysh$ cd /tmp && pwd
/tmp
mysh$ exit
Chức năngLời gọi hệ thốngChương
Chạy chương trình ngoàifork, execvp, waitpidChương 40
Đường ống giữa lệnhpipe, dup2Chương 43
Chuyển hướng vào raopen, dup2Chương 41
Chạy nền và jobfork không chờ, waitpid WNOHANGChương 40
Ctrl+C, dọn zombiesigaction, SIGINT, SIGCHLDChương 44

#Kiến trúc

  1. Đọc dòng lệnh

    fgets hoặc dùng thư viện readline để có lịch sử và di chuyển con trỏ. Lịch sử lưu vào ~/.mysh_history.
  2. Tách token

    Chia dòng thành các từ, xử lý dấu nháy "...", '...', và thoát ký tự \. Đây là phần khó nhất, không phải phần fork.
  3. Phân tích cú pháp

    Nhận ra |, >, <, >>, &, &&. Xây một cây hoặc danh sách các lệnh nối bằng ống.
  4. Lệnh nội trú hay ngoài

    cd, exit, export phải chạy trong chính tiến trình shell, không fork. Còn lại thì fork và exec.
  5. Thực thi

    Với lệnh ngoài: fork, con thiết lập chuyển hướng rồi exec, cha chờ hoặc ghi nhận job nền.

#Pipe và chuyển hướng

Nối hai lệnh bằng pipe (POSIX)
/* ls | grep .c

   pipe() tao mot cap mo ta tep: p[0] doc, p[1] ghi.
   Noi dau RA cua ls vao dau VAO cua grep. */

int p[2];
pipe(p);

if (fork() == 0) {                  /* con thu nhat: ls */
    dup2(p[1], STDOUT_FILENO);      /* stdout -> dau ghi cua pipe */
    close(p[0]); close(p[1]);       /* dong ca hai ban sao */
    execvp("ls", (char*[]){"ls", NULL});
    _exit(127);
}

if (fork() == 0) {                  /* con thu hai: grep */
    dup2(p[0], STDIN_FILENO);       /* stdin <- dau doc cua pipe */
    close(p[0]); close(p[1]);
    execvp("grep", (char*[]){"grep", ".c", NULL});
    _exit(127);
}

close(p[0]); close(p[1]);           /* CHA phai dong CA HAI dau pipe */
wait(NULL); wait(NULL);
Chuyển hướngMở tệp thế nàodup2 vào
> tepopen O_WRONLY O_CREAT O_TRUNCSTDOUT_FILENO
>> tepopen O_WRONLY O_CREAT O_APPENDSTDOUT_FILENO
< tepopen O_RDONLYSTDIN_FILENO
2> tepopen O_WRONLY O_CREAT O_TRUNCSTDERR_FILENO
2>&1không mở tệpdup2 STDOUT vào STDERR

#Tín hiệu và zombie

#Yêu cầu

Tự làm thử

  1. Tách token xử lý dấu nháy "...", '...', và thoát \.
  2. Pipe nhiều tầng không giới hạn.
  3. Chuyển hướng >, >>, <, 2>, 2>&1.
  4. Chạy nền &, lệnh jobs, fg, bg.
  5. Lệnh nội trú: cd, pwd, exit, export, unset, history, help.
  6. Biến môi trường, mở rộng $VAR và ~.
  7. Ctrl+C giết tiến trình con không giết shell; Ctrl+D thoát.
  8. Dọn zombie bằng bộ xử lý SIGCHLD.
  9. Không rò rỉ mô tả tệp, kiểm tra bằng /proc/PID/fd.
  10. Lịch sử lệnh lưu vào ~/.mysh_history.

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

  • Mini Shell là dự án POSIX; làm trên Linux, macOS, hoặc WSL vì cần fork và sigaction.
  • Lệnh nội trú như cd phải chạy trong chính shell, không fork, vì chúng đổi trạng thái của shell.
  • Mỗi tiến trình phải đóng các đầu pipe không dùng; rò rỉ mô tả tệp làm shell treo hoặc hết fd.
  • Shell bỏ qua SIGINT cho chính nó, dùng nhóm tiến trình để Ctrl+C tới đúng lệnh, và dọn zombie bằng SIGCHLD.
  • Xong khi đường ống nhiều tầng chạy, cd đổi thư mục, không rò rỉ fd, không zombie.