5. Phần xen kẽ: Process API
GHI CHÚ: VỀ CÁC PHẦN XEN KẼ
Các phần xen kẽ (interlude) sẽ đề cập đến những khía cạnh thực tiễn hơn của hệ thống, đặc biệt tập trung vào các API (Application Programming Interface – interface lập trình ứng dụng) của hệ điều hành và cách sử dụng chúng. Nếu bạn không thích các vấn đề mang tính thực hành, bạn có thể bỏ qua các phần này. Tuy nhiên, bạn nên quan tâm đến chúng, vì chúng thường rất hữu ích trong thực tế; ví dụ, các công ty thường không tuyển dụng bạn chỉ vì những kỹ năng “phi thực tế”.
Trong phần xen kẽ này, chúng ta sẽ thảo luận về việc tạo process (process) trong các hệ thống UNIX. UNIX cung cấp một trong những cách thú vị nhất để tạo process mới thông qua một cặp system call (call hệ thống): fork()
và exec()
. Một thủ tục thứ ba, wait()
, có thể được một process sử dụng khi muốn chờ process mà nó đã tạo hoàn thành. Chúng ta sẽ trình bày chi tiết các interface này, kèm theo một số ví dụ đơn giản để minh họa. Và đây là vấn đề đặt ra:
Cốt lõi: Làm thế nào để tạo và điều khiển process
Hệ điều hành nên cung cấp những interface nào để tạo và điều khiển process? Các interface này nên được thiết kế ra sao để vừa mạnh mẽ, dễ sử dụng, vừa đạt hiệu năng cao?
5.1 Lời gọi hệ thống fork()
Lời gọi hệ thống fork()
được sử dụng để tạo một process mới [C63]. Tuy nhiên, hãy lưu ý: đây chắc chắn là một trong những thủ tục kỳ lạ nhất mà bạn từng gọi1. Cụ thể hơn, giả sử bạn có một chương trình đang chạy với đoạn code như trong Hình 5.1; hãy xem xét đoạn code đó, hoặc tốt hơn, hãy gõ và chạy nó để tự mình trải nghiệm.
Thực ra, chúng tôi không thể khẳng định chắc chắn điều này; ai biết được bạn gọi những thủ tục gì khi không ai để ý? Nhưng fork()
thực sự rất khác thường, bất kể thói quen gọi hàm của bạn ra sao.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char * argv[]) {
printf("hello (pid:%d)\n", (int) getpid());
int rc = fork();
if (rc < 0) {
// fork failed
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) {
// child (new process)
printf("child (pid:%d)\n", (int) getpid());
} else {
// parent goes down this path (main)
printf("parent of %d (pid:%d)\n",
rc, (int) getpid());
}
return 0;
}
Figure 5.1: Calling fork() (p1.c)
Khi bạn chạy chương trình này (gọi là p1.c
), bạn sẽ thấy kết quả như sau:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Hãy phân tích chi tiết hơn điều gì đã xảy ra trong p1.c
. Khi bắt đầu chạy, process in ra một thông điệp “hello”; kèm theo đó là process identifier (PID – định danh process). Trong ví dụ, process có PID là 29146; trong hệ thống UNIX, PID được dùng để định danh process khi muốn thực hiện một thao tác nào đó với nó, chẳng hạn như dừng process. Đến đây thì mọi thứ vẫn bình thường.
Phần thú vị bắt đầu khi process gọi call hệ thống fork()
, mà OS cung cấp như một cách để tạo process mới. Điều kỳ lạ là: process được tạo ra gần như là bản sao của process gọi fork()
. Điều này có nghĩa là, từ góc nhìn của OS, giờ đây có hai bản sao của chương trình p1
đang chạy, và cả hai đều sắp trả về từ call fork()
. Process mới được tạo (gọi là child process – process con, đối lập với parent process – process cha) không bắt đầu chạy từ main()
như bạn có thể nghĩ (lưu ý, thông điệp “hello” chỉ được in ra một lần); thay vào đó, nó xuất hiện như thể chính nó vừa gọi fork()
.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char * argv[]) {
printf("hello (pid:%d)\n", (int) getpid());
int rc = fork();
if (rc < 0) {
// fork failed; exit
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) { // child (new process)
printf("child (pid:%d)\n", (int) getpid());
} else {
// parent goes down this path
int rc_wait = wait(NULL);
printf("parent of %d (rc_wait:%d) (pid:%d)\n",
rc, rc_wait, (int) getpid());
}
return 0;
}
Figure 5.2: Calling fork() And wait() (p2.c)
Bạn có thể nhận thấy: process con không phải là bản sao hoàn toàn. Cụ thể, mặc dù nó có bản sao riêng của address space (không gian địa chỉ – tức vùng bộ nhớ riêng), các thanh ghi (registers) riêng, bộ đếm chương trình (PC – Program Counter) riêng, v.v., nhưng giá trị trả về từ fork()
lại khác nhau. Cụ thể, process cha nhận về PID của process con mới tạo, còn process con nhận giá trị trả về là 0. Sự khác biệt này rất hữu ích, vì nó giúp lập trình viên dễ dàng viết mã xử lý cho hai trường hợp khác nhau.
Bạn cũng có thể nhận thấy: kết quả in ra từ p1.c
không mang tính xác định (non-deterministic). Khi process con được tạo, lúc này có hai process đang hoạt động: cha và con. Giả sử hệ thống chỉ có một CPU (để đơn giản), thì tại thời điểm đó, hoặc process cha, hoặc process con có thể được chạy trước. Trong ví dụ trên, process cha chạy trước và in thông điệp của nó trước. Trong các trường hợp khác, điều ngược lại có thể xảy ra, như trong kết quả sau:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
CPU scheduler (bộ lập lịch CPU – thành phần quyết định process nào chạy tại một thời điểm) sẽ quyết định process nào được chạy. Vì bộ lập lịch khá phức tạp, chúng ta thường không thể dự đoán chắc chắn nó sẽ chọn process nào, và do đó không thể biết process nào sẽ chạy trước. Tính bất định (nondeterminism) này sẽ dẫn đến nhiều vấn đề thú vị, đặc biệt trong các chương trình đa luồng (multi-threaded). Chúng ta sẽ gặp lại khái niệm này nhiều hơn khi nghiên cứu lập trình đồng thời (concurrency) ở phần sau của sách.
5.2 Lời gọi hệ thống wait()
Cho đến giờ, chúng ta mới chỉ tạo một process con in ra thông điệp rồi thoát. Trong nhiều trường hợp, process cha cần chờ process con hoàn thành công việc. Nhiệm vụ này được thực hiện bằng call hệ thống wait()
(hoặc phiên bản đầy đủ hơn là waitpid()
); xem Hình 5.2 để biết chi tiết.
Trong ví dụ (p2.c
), process cha gọi wait()
để tạm dừng excecute cho đến khi process con kết thúc. Khi process con hoàn tất, wait()
trả quyền điều khiển lại cho process cha.
Việc thêm call wait()
vào đoạn code trên khiến kết quả trở nên xác định (deterministic). Bạn có biết tại sao không? Hãy thử suy nghĩ một chút.
(… chờ bạn suy nghĩ … xong)
Và đây là kết quả:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
Với đoạn code này, chúng ta luôn biết rằng process con sẽ in ra trước. Tại sao? Bởi vì:
- Nếu process con chạy trước, nó sẽ in thông điệp trước process cha.
- Nếu process cha chạy trước, nó sẽ lập tức gọi
wait()
; call này sẽ không trả về cho đến khi process con chạy và thoát2. Do đó, ngay cả khi cha chạy trước, nó vẫn “lịch sự” chờ con hoàn thành, rồi mới in thông điệp của mình.
Có một số trường hợp wait()
trả về trước khi process con thoát; hãy đọc trang hướng dẫn man
để biết thêm chi tiết. Và hãy cảnh giác với những phát biểu tuyệt đối như “process con luôn in trước” hoặc “UNIX là thứ tuyệt vời nhất trên đời, còn hơn cả kem”.
5.3 Cuối cùng, call hệ thống exec()
Một thành phần cuối cùng và quan trọng của process creation API (API tạo process) là call hệ thống exec()
3. Lời gọi này hữu ích khi bạn muốn chạy một chương trình khác với chương trình đang gọi nó. Ví dụ, gọi fork()
…
Trên Linux, có sáu biến thể của exec()
: execl()
, execlp()
, execle()
, execv()
, execvp()
, và execvpe()
. Hãy đọc trang hướng dẫn man
để tìm hiểu thêm.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main(int argc, char * argv[]) {
printf("hello (pid:%d)\n", (int) getpid());
int rc = fork();
if (rc < 0) {
// fork failed; exit
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) { // child (new process)
printf("child (pid:%d)\n", (int) getpid());
char * myargs[3];
myargs[0] = strdup("wc");
// program: "wc"
myargs[1] = strdup("p3.c"); // arg: input file
myargs[2] = NULL;
// mark end of array
execvp(myargs[0], myargs); // runs word count
printf("this shouldn’t print out");
} else {
// parent goes down this path
int rc_wait = wait(NULL);
printf("parent of %d (rc_wait:%d) (pid:%d)\n",
rc, rc_wait, (int) getpid());
}
return 0;
}
Hình 5.3: Gọi fork(), wait() và exec() (p3.c)
Trong p2.c
, việc sử dụng chỉ hữu ích nếu bạn muốn tiếp tục chạy các bản sao của cùng một chương trình. Tuy nhiên, trong nhiều trường hợp, bạn muốn chạy một chương trình khác; exec()
chính là công cụ để làm điều đó (Hình 5.3).
Trong ví dụ này, process con (child process) gọi execvp()
để chạy chương trình wc
– đây là chương trình đếm từ (word counting program). Cụ thể, nó chạy wc
trên tệp mã nguồn p3.c
, cho chúng ta biết số dòng, số từ và số byte trong tệp:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29
107
1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
Lời gọi hệ thống (system call) fork()
vốn đã kỳ lạ; “đồng phạm” của nó, exec()
, cũng không hề bình thường. Cách hoạt động của exec()
như sau: khi được cung cấp tên của một tệp excecute (executable) – ví dụ wc
– và một số đối số (arguments) – ví dụ p3.c
– nó sẽ nạp (load) mã lệnh (code) và static data (static data) từ tệp excecute đó, ghi đè lên phân đoạn code (code segment) và static data hiện tại của process; vùng heap, stack và các phần khác của không gian bộ nhớ (memory space) của chương trình sẽ được khởi tạo lại. Sau đó, hệ điều hành chỉ đơn giản chạy chương trình mới này, truyền các đối số vào dưới dạng argv
của process.
Điểm quan trọng: exec()
không tạo ra process mới; thay vào đó, nó biến đổi process hiện tại (trước đó là p3
) thành một chương trình khác đang chạy (wc
). Sau khi exec()
được gọi trong process con, gần như p3.c
chưa từng chạy; một call exec()
thành công sẽ không bao giờ trả về.
Mẹo: Làm đúng ngay từ đầu (Lampson’s Law)
Như Lampson đã nói trong bài viết nổi tiếng Hints for Computer Systems Design [L83]:
“Hãy làm đúng. Cả tính trừu tượng lẫn sự đơn giản đều không thể thay thế cho việc làm đúng.”
Đôi khi, bạn chỉ cần làm đúng, và khi làm đúng, kết quả sẽ vượt xa mọi lựa chọn thay thế. Có rất nhiều cách để thiết kế API cho việc tạo process; tuy nhiên, sự kết hợp giữafork()
vàexec()
vừa đơn giản vừa cực kỳ mạnh mẽ. Ở đây, các nhà thiết kế UNIX đã “làm đúng”. Và vì Lampson thường xuyên “làm đúng”, nên nguyên tắc này được đặt tên để vinh danh ông.
5.4 Tại sao? Động cơ thiết kế API
Một câu hỏi lớn có thể xuất hiện: Tại sao chúng ta lại xây dựng một interface kỳ lạ như vậy cho một hành động tưởng chừng đơn giản là tạo process mới?
Câu trả lời: việc tách biệt fork()
và exec()
là thiết yếu trong việc xây dựng một UNIX shell, vì nó cho phép shell chạy một đoạn code sau khi gọi fork()
nhưng trước khi gọi exec()
. Đoạn code này có thể thay đổi môi trường (environment) của chương trình sắp chạy, từ đó cho phép xây dựng nhiều tính năng thú vị một cách dễ dàng.
Shell thực chất chỉ là một chương trình người dùng (user program)4. Nó hiển thị một dấu nhắc lệnh (prompt) và chờ bạn nhập vào. Bạn gõ một lệnh (tức tên của một chương trình excecute kèm các đối số) vào; trong hầu hết các trường hợp, shell sẽ:
- Xác định vị trí của tệp excecute trong hệ thống tệp (file system).
- Gọi
fork()
để tạo một process con chạy lệnh đó. - Gọi một biến thể của
exec()
để excecute lệnh. - Gọi
wait()
để chờ lệnh hoàn tất.
Khi process con kết thúc, shell thoát khỏi wait()
và in ra dấu nhắc lệnh mới, sẵn sàng cho lệnh tiếp theo.
Việc tách fork()
và exec()
cho phép shell thực hiện nhiều việc hữu ích một cách đơn giản. Ví dụ:
prompt> wc p3.c > newfile.txt
Có rất nhiều loại shell: tcsh
, bash
, zsh
… Bạn nên chọn một loại, đọc trang hướng dẫn (man page
) và tìm hiểu thêm; tất cả chuyên gia UNIX đều làm vậy.
Trong ví dụ trên, đầu ra của chương trình wc
được chuyển hướng (redirect) vào tệp newfile.txt
(dấu >
thể hiện việc chuyển hướng). Cách shell thực hiện rất đơn giản: khi process con được tạo, trước khi gọi exec()
, shell (cụ thể là đoạn code chạy trong process con) đóng luồng xuất chuẩn (standard output) và mở tệp newfile.txt
.
Bằng cách này, mọi dữ liệu xuất ra từ chương trình wc
sắp chạy sẽ được ghi vào tệp thay vì hiển thị trên màn hình. (Các file descriptor đang mở sẽ vẫn được giữ nguyên qua call exec()
, cho phép hành vi này [SR05]).
Hình 5.4 (trang 8) minh họa một chương trình thực hiện chính xác điều này. Nguyên nhân việc chuyển hướng hoạt động được là nhờ giả định về cách hệ điều hành quản lý file descriptor: trong UNIX, hệ thống bắt đầu tìm file descriptor trống từ số 0. Trong trường hợp này, STDOUT_FILENO
sẽ là file descriptor trống đầu tiên và được gán khi open()
được gọi. Các lệnh ghi (write) tiếp theo của process con tới file descriptor xuất chuẩn – ví dụ qua printf()
– sẽ được ghi vào tệp mới mở thay vì màn hình.
Kết quả chạy chương trình p4.c
như sau:
prompt> ./p4
prompt> cat p4.output
32
109
846 p4.c
prompt>
Có ít nhất hai điểm thú vị trong kết quả này:
- Khi chạy
p4
, có vẻ như không có gì xảy ra; shell chỉ in dấu nhắc lệnh và sẵn sàng cho lệnh tiếp theo. Nhưng thực tế,p4
đã gọifork()
để tạo process con, sau đó chạy chương trìnhwc
quaexecvp()
. Bạn không thấy kết quả trên màn hình vì nó đã được chuyển hướng vào tệpp4.output
. - Khi dùng
cat
để xem nội dung tệpp4.output
, toàn bộ kết quả mong đợi từwc
đều có ở đó.
UNIX pipes (đường ống) được triển khai tương tự, nhưng sử dụng call hệ thống pipe()
. Trong trường hợp này, đầu ra của một process được kết nối tới một in-kernel pipe (hàng đợi trong nhân hệ điều hành), và đầu vào của process khác được kết nối tới cùng pipe đó. Nhờ vậy, đầu ra của process này trở thành đầu vào của process kia một cách liền mạch, cho phép ghép chuỗi nhiều lệnh hữu ích.
Ví dụ đơn giản: tìm một từ trong tệp và đếm số lần từ đó xuất hiện. Với pipe và các tiện ích grep
và wc
, việc này rất dễ:
grep -o foo file | wc -l
Nhập lệnh trên vào shell và bạn sẽ thấy kết quả ngay.
Cuối cùng, mặc dù chúng ta mới chỉ phác thảo API tạo process ở mức cao, nhưng vẫn còn rất nhiều chi tiết cần tìm hiểu thêm; ví dụ, chúng ta sẽ học kỹ hơn về file descriptor khi bàn về hệ thống tệp ở phần ba của sách. Tạm thời, có thể kết luận rằng sự kết hợp fork()
/exec()
là một cách mạnh mẽ để tạo và điều khiển process.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/wait.h>
int main(int argc, char * argv[]) {
int rc = fork();
if (rc < 0) {
// fork failed
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) {
// child: redirect standard output to a file
close(STDOUT_FILENO);
open("./p4.output", O_CREAT | O_WRONLY | O_TRUNC,
S_IRWXU);
// now exec "wc"...
char * myargs[3];
myargs[0] = strdup("wc");
// program: wc
myargs[1] = strdup("p4.c"); // arg: file to count
myargs[2] = NULL;
// mark end of array
execvp(myargs[0], myargs); // runs word count
} else {
// parent goes down this path (main)
int rc_wait = wait(NULL);
}
return 0;
}
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Điều khiển process (Process Control) và Người dùng (Users)
Ngoài fork()
, exec()
và wait()
, trong các hệ thống UNIX còn có nhiều interface khác để tương tác với process (process). Ví dụ, call hệ thống (system call) kill()
được dùng để gửi signal (tín hiệu) tới một process, bao gồm các chỉ thị như tạm dừng (pause), kết thúc (die/terminate) và nhiều lệnh hữu ích khác.
Để thuận tiện, trong hầu hết các UNIX shell, một số tổ hợp phím được cấu hình để gửi một tín hiệu cụ thể tới process đang chạy. Ví dụ:
Ctrl-C
gửi tín hiệu SIGINT (interrupt – ngắt) tới process (thường sẽ kết thúc process đó).Ctrl-Z
gửi tín hiệu SIGTSTP (stop – dừng) để tạm dừng process khi đang excecute (bạn có thể tiếp tục process này sau bằng một lệnh, ví dụ lệnh tích hợpfg
có trong nhiều shell).
Toàn bộ hệ thống con signals (signals subsystem) cung cấp một hạ tầng phong phú để gửi các sự kiện bên ngoài tới process, bao gồm:
- Cách để process nhận và xử lý các tín hiệu này.
- Cách gửi tín hiệu tới từng process hoặc cả nhóm process (process group).
Để sử dụng hình thức giao tiếp này, một process có thể dùng call hệ thống signal()
để “bắt” (catch) các tín hiệu khác nhau. Khi một tín hiệu cụ thể được gửi tới process, process sẽ tạm ngừng excecute bình thường và chạy một đoạn code được chỉ định để xử lý tín hiệu đó. (Xem [SR05] để tìm hiểu sâu hơn về signals và các chi tiết phức tạp của chúng).
GHI CHÚ: RTFM — Read The Man Pages
Nhiều lần trong cuốn sách này, khi đề cập đến một system call hoặc library call cụ thể, chúng tôi sẽ khuyên bạn đọc man pages (manual pages – trang hướng dẫn). Đây là dạng tài liệu gốc tồn tại trên các hệ thống UNIX, được tạo ra trước khi có thứ gọi là “web”.
Dành thời gian đọc man pages là một bước quan trọng trong quá trình trưởng thành của một lập trình viên hệ thống; có rất nhiều thông tin hữu ích ẩn trong đó. Một số man pages đặc biệt hữu ích gồm:
- Man pages của shell bạn đang dùng (ví dụ:
tcsh
,bash
). - Man pages của bất kỳ system call nào mà chương trình của bạn sử dụng (để biết giá trị trả về và các điều kiện lỗi).
Cuối cùng, đọc man pages còn giúp bạn tránh bối rối. Khi bạn hỏi đồng nghiệp về một chi tiết phức tạp của fork()
, họ có thể chỉ trả lời: “RTFM”. Đây là cách họ nhẹ nhàng nhắc bạn Read The Man pages. Chữ “F” trong RTFM chỉ để thêm chút “màu sắc” cho câu nói…
Điều này dẫn đến câu hỏi: Ai có thể gửi tín hiệu tới một process, và ai thì không?
Thông thường, hệ thống có thể được nhiều người dùng cùng lúc. Nếu một người có thể tùy ý gửi tín hiệu như SIGINT (để ngắt process, thường sẽ kết thúc nó) tới process của người khác, tính khả dụng (usability) và bảo mật (security) của hệ thống sẽ bị ảnh hưởng nghiêm trọng.
Vì vậy, các hệ thống hiện đại có khái niệm chặt chẽ về user (người dùng). Sau khi nhập mật khẩu để xác thực, người dùng đăng nhập để truy cập tài nguyên hệ thống. Người dùng có thể khởi chạy một hoặc nhiều process và toàn quyền điều khiển chúng (tạm dừng, kết thúc, v.v.).
Người dùng thường chỉ có thể điều khiển process của chính mình; hệ điều hành chịu trách nhiệm phân bổ tài nguyên (CPU, bộ nhớ, đĩa…) cho từng người dùng (và các process của họ) để đạt được mục tiêu tổng thể của hệ thống.
5.6 Các công cụ hữu ích
Có nhiều công cụ dòng lệnh (command-line tools) rất hữu ích. Ví dụ:
- Lệnh
ps
cho phép bạn xem các process đang chạy; hãy đọc man pages để biết các tùy chọn (flags) hữu ích khi dùngps
. - Công cụ
top
cũng rất hữu ích, hiển thị các process của hệ thống và mức độ sử dụng CPU cùng các tài nguyên khác. Thú vị là, nhiều khi bạn chạytop
, nó tự nhận mình là process “ngốn” tài nguyên nhất – có lẽ hơi… tự mãn. - Lệnh
kill
có thể được dùng để gửi tín hiệu tùy ý tới process;killall
thân thiện hơn một chút với người dùng.
Hãy cẩn thận khi dùng các lệnh này; nếu bạn vô tình “kill” window manager (trình quản lý cửa sổ), máy tính trước mặt bạn có thể trở nên rất khó sử dụng.
Ngoài ra, có nhiều loại CPU meter (đồng hồ đo CPU) giúp bạn nhanh chóng nắm được tải (load) của hệ thống. Ví dụ, chúng tôi luôn bật MenuMeters (từ Raging Menace software) trên thanh công cụ của máy Mac để xem mức sử dụng CPU tại mọi thời điểm. Nói chung, càng có nhiều thông tin về những gì đang diễn ra, bạn càng dễ quản lý hệ thống.
GHI CHÚ: Superuser (Root)
Một hệ thống thường cần một người dùng có quyền quản trị (admin) và không bị giới hạn như người dùng thông thường. Người này có thể:
- Kết thúc (kill) bất kỳ process nào (ví dụ: nếu process đó đang gây hại cho hệ thống), ngay cả khi process đó không do họ khởi chạy.
- Chạy các lệnh mạnh như
shutdown
(tắt hệ thống).
Trong các hệ thống dựa trên UNIX, các quyền đặc biệt này được trao cho superuser (còn gọi là root). Trong khi hầu hết người dùng không thể kết thúc process của người khác, superuser có thể.
Làm root giống như làm Spider-Man: “Quyền lực lớn đi kèm trách nhiệm lớn” [QI15]. Vì vậy, để tăng cường bảo mật (và tránh sai lầm tốn kém), tốt nhất là bạn nên hoạt động như một người dùng bình thường; nếu cần trở thành root, hãy thật cẩn trọng, vì mọi “quyền năng hủy diệt” của thế giới máy tính đều nằm trong tay bạn.
5.7 Tóm tắt
Chúng ta đã giới thiệu một số API liên quan đến việc tạo process trong UNIX: fork()
, exec()
và wait()
. Tuy nhiên, đây mới chỉ là phần bề mặt. Để tìm hiểu chi tiết hơn, hãy đọc Stevens và Rago [SR05], đặc biệt các chương về Process Control, Process Relationships và Signals.
Mặc dù chúng tôi đánh giá cao API process của UNIX, nhưng không phải ai cũng đồng tình. Ví dụ, một bài báo gần đây của các nhà nghiên cứu hệ thống từ Microsoft, Đại học Boston và ETH Zurich đã chỉ ra một số vấn đề với fork()
và đề xuất các API tạo process khác, đơn giản hơn, như spawn()
[B+19]. Hãy đọc bài báo và các tài liệu liên quan để hiểu góc nhìn khác này.
Nhớ rằng, dù bạn có thể tin tưởng cuốn sách này, tác giả vẫn có quan điểm riêng; và những quan điểm đó không phải lúc nào cũng được chia sẻ rộng rãi như bạn nghĩ.
GHI CHÚ: Các thuật ngữ chính trong Process API
- Mỗi process (process) có một tên; trong hầu hết các hệ thống, tên này là một số gọi là process ID (PID – định danh process).
- Lời gọi hệ thống
fork()
trong UNIX được dùng để tạo process mới. Process tạo ra gọi là parent (cha), process mới gọi là child (con). Giống như ngoài đời [J16], process con gần như là bản sao của process cha. - Lời gọi hệ thống
wait()
cho phép process cha chờ process con hoàn tất excecute. - Nhóm call hệ thống
exec()
cho phép process con thoát khỏi sự giống nhau với process cha và excecute một chương trình hoàn toàn mới. - Một UNIX shell thường dùng
fork()
,wait()
vàexec()
để chạy lệnh của người dùng; việc táchfork
vàexec
cho phép thực hiện các tính năng như chuyển hướng I/O (input/output redirection), pipes và nhiều tính năng khác mà không cần
Tham khảo
[C63] “A Multiprocessor System Design”
Melvin E. Conway
AFIPS ’63 Fall Joint Computer Conference
New York, USA 1963
An early paper on how to design multiprocessing systems; may be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations”
Jack B. Dennis and Earl C. Van Horn
Communications of the ACM, Volume 9, Number 3, March 1966
A classic paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence on Project MAC, Multics, and eventually UNIX.
[L83] “Hints for Computer Systems Design”
Butler Lampson
ACM Operating Systems Review, 15:5, October 1983
Lampson’s famous hints on how to design computer systems. You should read it at some point in your life, and probably at many points in your life.
[SR05] “Advanced Programming in the UNIX Environment”
W. Richard Stevens and Stephen A. Rago
Addison-Wesley, 2005
All nuances and subtleties of using UNIX APIs are found herein. Buy this book! Read it! And most importantly, live it.