The Linux Programming Interface Chương 27 · Program Execution

Chương 27 · Thực thi chương trình Chapter 27 · Program Execution

Nạp một chương trình mới vào chính tiến trình đang chạy Thay cả bộ não của tiến trình bằng một chương trình khác Loading a new program into the very process that is running

Chương 26 cho ta fork() để tạo một tiến trình; chương này cho ta execve() để khiến một tiến trình bỏ chương trình hiện tại và chạy một chương trình hoàn toàn mới trong cùng cái vỏ tiến trình đó — giữ nguyên PID nhưng thay mới stack, heap, data và text.12 Quanh execve(), Kerrisk xếp bảy hàm thư viện tiện dụng (execle, execlp, execvp, execv, execl), chuyện script thông dịch với dòng #!, số phận của file descriptor và tín hiệu khi exec, rồi khép lại bằng system() — cách chạy một lệnh shell và vì sao hiện thực nó cho đúng lại khó.1

Ở chương trước, fork() giống như nhân bản một người thành hai. Chương này làm việc khác: giữ nguyên con người đó nhưng thay toàn bộ trí nhớ và công việc của họ bằng một nhiệm vụ mới. Công cụ chính tên là execve(). Quanh nó có vài phiên bản gọi cho tiện, chuyện tệp script bắt đầu bằng #!, và một hàm gọn tên system() để chạy lệnh shell. Nhớ nhé: fork tạo thêm tiến trình, còn exec thay chương trình bên trong một tiến trình.

Chapter 26 gave us fork() to create a process; this chapter gives us execve() to make a process abandon its current program and run a completely new one inside the same process shell — keeping the PID but replacing the stack, heap, data, and text.12 Around execve(), Kerrisk arranges seven convenience library functions (execle, execlp, execvp, execv, execl), interpreter scripts and the #! line, the fate of file descriptors and signals across exec, and finally system() — how to run a shell command and why implementing it correctly is hard.1

Điểm nối mạch của chương: fork() + exec() + wait() là bộ ba làm nên gần như mọi thứ trong UNIX. Một shell khi chạy lệnh của bạn chính là fork ra con, cho con exec thành chương trình bạn gõ, rồi cha đợi con. Chương 27 chỉ là một phần ba của bộ ba đó — phần "biến con thành chương trình mới" — nên mọi ví dụ ở đây đều ngầm giả định tiến trình gọi exec thường là một tiến trình con vừa được fork ra.1

Luận đề của chương: execve() không tạo tiến trình mới mà thay chương trình đang chạy bằng một chương trình khác trong cùng tiến trình; mọi hàm exec*() khác chỉ là lớp bọc tiện dụng khác nhau ở cách truyền đối số, có tìm PATH hay không, và có truyền môi trường hay không; qua ranh giới exec, file descriptor mặc định sống sót trừ khi đánh dấu close-on-exec, còn trình xử lý tín hiệu bị trả về mặc định — và system() gói trọn fork+exec+wait lại cho bạn.12

Ý chính: exec không đẻ ra tiến trình mới, nó thay chương trình bên trong một tiến trình. Các phiên bản exec chỉ khác nhau ở cách bạn đưa đối số và môi trường. Qua exec, các tệp đang mở vẫn mở (trừ khi bạn dặn đóng), còn cách xử lý tín hiệu thì bị xoá. Nhớ nhé: exec = thay chương trình, giữ nguyên tiến trình.

The chapter's thesis: execve() does not create a new process but replaces the running program with another inside the same process; every other exec*() function is a convenience wrapper differing in how it passes arguments, whether it searches PATH, and whether it passes an environment; across the exec boundary file descriptors survive by default unless marked close-on-exec, while signal handlers are reset to default — and system() packages fork+exec+wait for you.12

Cụm khái niệm 01 Concept cluster 01

execve() thay chương trình đang chạy — và nếu thành công thì không bao giờ quay về execve() thay chương trình, và khi thành công thì nó "đi luôn" execve() replaces the running program — and on success it never returns

execve() nạp một chương trình mới vào bộ nhớ của tiến trình gọi. Khi làm vậy, chương trình cũ bị vứt bỏ, và stack, data cùng heap của tiến trình được thay bằng của chương trình mới.12 Nguyên mẫu là int execve(const char *pathname, char *const argv[], char *const envp[]): pathname là đường dẫn tới chương trình, argv là mảng đối số dòng lệnh kết thúc bằng NULL, envp là danh sách biến môi trường dạng name=value cũng kết thúc bằng NULL.127

Hãy hình dung tiến trình như một căn phòng. execve() không xây phòng mới; nó dọn sạch đồ đạc cũ trong phòng và bày vào đồ của một chương trình khác. Người ở phòng (tiến trình) vẫn là người đó, số nhà (PID) vẫn vậy, nhưng mọi thứ bên trong đã mới hoàn toàn. Bạn đưa cho nó: đường dẫn chương trình, danh sách đối số, và danh sách biến môi trường. Nhớ nhé: execve giữ căn phòng, thay toàn bộ đồ đạc bên trong.

execve() loads a new program into the calling process's memory. In doing so, the old program is discarded, and the process's stack, data, and heap are replaced by those of the new program.12 The prototype is int execve(const char *pathname, char *const argv[], char *const envp[]): pathname is the path to the program, argv is the NULL-terminated array of command-line arguments, and envp is the list of environment variables as name=value strings, also NULL-terminated.127

Định nghĩa. Vì execve() thay toàn bộ chương trình đang chạy, một lời gọi thành công không bao giờ trở về — không có dòng mã nào sau nó được chạy. Nó chỉ trở về khi thất bại, và khi đó trả về −1 cùng errno cho biết lý do.12

Nói đơn giản. Nếu execve chạy được, chương trình cũ biến mất nên chẳng còn ai để "quay về". Chỉ khi nó hỏng bạn mới thấy nó trả về −1. Nhớ nhé: thấy execve trả về, nghĩa là nó đã thất bại.

Definition. Because execve() replaces the entire running program, a successful call never returns — no line of code after it runs. It returns only on failure, when it yields −1 with errno set to the reason.12

char *argv[] = {"ls", "-l", NULL};
char *envp[] = {"PATH=/bin", NULL};
execve("/bin/ls", argv, envp);
/* chỉ chạy tới đây NẾU lỗi */
perror("execve"); _exit(127);

execve("/bin/ls", argv, envp);
/* chạy tới đây = đã hỏng */
perror("execve");

char *argv[] = {"ls", "-l", NULL};
char *envp[] = {"PATH=/bin", NULL};
execve("/bin/ls", argv, envp);
/* reached only IF it failed */
perror("execve"); _exit(127);

  1. replace, not create

    Thay, chứ không tạo — PID giữ nguyên Thay chương trình, không sinh thêm tiến trình Replace, not create — the PID stays

    Đây là ranh giới dễ nhầm nhất: fork() tạo một tiến trình mới, còn exec() thay chương trình chạy trong một tiến trình đã có. Sau một lần exec thành công, tiến trình vẫn giữ nguyên PID của mình; chỉ có nội dung chương trình là mới.12

    Nhiều người tưởng exec "mở thêm một chương trình nữa". Không phải. Nó thay cái đang chạy bằng cái mới, ngay trong cùng một tiến trình. Số định danh tiến trình (PID) không đổi. Nhớ nhé: muốn chạy song song thì fork trước, rồi mới exec ở con.

    This is the most easily confused boundary: fork() creates a new process, while exec() replaces the program running in an existing process. After a successful exec the process keeps its PID; only the program content is new.12

    Bạn gặp nó ở đâuWhere you meet it Mỗi lần shell chạy một lệnh: nó fork() ra một con, rồi cho con exec() thành chương trình bạn gõ — nên lệnh chạy trong một PID riêng mà shell vẫn sống.1 Khi bạn gõ một lệnh trong terminal: shell nhân bản ra một con rồi biến con đó thành lệnh của bạn. Every time a shell runs a command: it fork()s a child, then has the child exec() into your program — so the command runs in its own PID while the shell lives on.1
  2. argv & envp

    argv và envp trở thành đối số và môi trường của chương trình mới Danh sách đối số và biến môi trường của chương trình mới argv and envp become the new program's arguments and environment

    Mảng argv bạn truyền sẽ trở thành argv của chương trình mới, và theo quy ước argv[0] nên chứa tên của chính chương trình được chạy (thường là phần tên tệp). Mảng envp trở thành danh sách môi trường (environ) của chương trình mới. Cả hai đều phải kết thúc bằng con trỏ NULL.123

    Những gì bạn đưa vào argv sẽ là các "tham số dòng lệnh" mà chương trình mới nhận được, giống hệt khi bạn gõ lệnh kèm tùy chọn. Ô đầu tiên (argv[0]) theo lệ là tên chương trình. envp là danh sách biến môi trường của nó. Nhớ nhé: cả hai danh sách đều phải có ô cuối là NULL để báo "hết".

    The argv array you pass becomes the new program's argv, and by convention argv[0] should hold the name of the program being run (usually the filename). The envp array becomes the new program's environment list (environ). Both must be terminated by a NULL pointer.123

    Bạn gặp nó ở đâuWhere you meet it Khi một chương trình hành xử khác nhau tùy argv[0] (ví dụ vi và view cùng một tệp thực thi) — đó là nhờ ô argv[0] mà người gọi exec đặt vào. Khi một lệnh đổi cách chạy tùy theo cái tên dùng để gọi nó — đó là do ô đầu tiên của danh sách đối số. When a program behaves differently depending on argv[0] (e.g. vi vs view being the same binary) — that is the argv[0] slot the exec caller supplied.
  3. point of no return

    Điểm không quay lại — và những gì sống sót Qua exec rồi thì không lùi được The point of no return — and what survives

    Sau một exec thành công, text, data, heap và stack của chương trình cũ biến mất — mọi biến, mọi con trỏ hàm, mọi handler bạn đã cài đều không còn. Nhưng tiến trình vẫn là một tiến trình, nên nhiều thuộc tính được giữ: PID, quan hệ cha–con, các file descriptor đang mở, umask, thư mục làm việc hiện tại, và nhiều thứ nữa.12

    Khi exec xong, mọi thứ "trong đầu" chương trình cũ đều mất: biến, hàm, cách xử lý tín hiệu. Nhưng vì vẫn là cùng một tiến trình, nên các thứ thuộc về cái vỏ tiến trình thì còn: số định danh, các tệp đang mở, thư mục đang đứng… Nhớ nhé: nội dung chương trình thì mất, nhưng vỏ tiến trình và tài nguyên của nó thì ở lại.

    After a successful exec, the old program's text, data, heap, and stack vanish — every variable, every function pointer, every handler you installed is gone. But the process is still a process, so many attributes are preserved: the PID, the parent–child relationship, open file descriptors, the umask, the current working directory, and more.12

    Bạn gặp nó ở đâuWhere you meet it Đây là nền của mọi I/O redirection trong shell: shell sửa các file descriptor trước khi exec, và vì FD sống sót qua exec nên chương trình mới nhận đúng đầu vào/ra đã bị bẻ dòng. Đây là lý do lệnh có thể đọc/ghi vào tệp bạn chỉ định: shell chuẩn bị sẵn trước khi exec, và các tệp mở vẫn còn sau đó. This underlies all shell I/O redirection: the shell rearranges file descriptors before exec, and because FDs survive exec, the new program inherits the redirected input/output.

Cụm khái niệm 02 Concept cluster 02

Đọc tên hàm exec() như đọc công thức: l/v, e, p Tên hàm exec() đánh vần cho bạn biết nó làm gì Read the exec() names like a formula: l/v, e, p

Các biến thể exec*() tồn tại để gọi thuận tiện hơn execve(). Ba chữ cái trong tên cho biết chúng khác nhau thế nào: l (list) — đối số truyền thành một danh sách rời, kết thúc bằng (char *) NULL; v (vector) — đối số truyền thành một mảng argv; e (environment) — truyền thêm một mảng envp môi trường; p (PATH) — nếu tên tệp không chứa dấu gạch chéo thì tìm nó trong các thư mục liệt kê bởi biến môi trường PATH.13

Tất cả các hàm này rốt cuộc đều gọi execve(), chỉ khác cách bạn đưa thông tin vào. Cái tên giống như nhãn dán: chữ l nghĩa là liệt kê đối số từng cái một; chữ v nghĩa là đưa cả một mảng; chữ e nghĩa là có kèm danh sách môi trường; chữ p nghĩa là tự đi tìm chương trình trong PATH. Nhớ nhé: nhìn đuôi tên hàm là đoán được cách nó nhận đối số.

The exec*() variants exist to call execve() more conveniently. The letters in each name say how they differ: l (list) — arguments passed as a loose list terminated by (char *) NULL; v (vector) — arguments passed as an argv array; e (environment) — an extra envp environment array is passed; p (PATH) — if the filename contains no slash, it is sought in the directories listed in the PATH environment variable.13

Định nghĩa. Chỉ các hàm có chữ e (execle, execve) cho bạn chỉ định môi trường qua envp; các hàm còn lại truyền tiếp môi trường hiện có (environ) của tiến trình gọi. Chỉ các hàm có chữ p (execlp, execvp) thực hiện tìm theo PATH; các hàm khác đòi một đường dẫn tới đúng tệp.13

Nói đơn giản. Có chữ e thì bạn tự đưa môi trường; không có thì nó dùng môi trường sẵn có. Có chữ p thì bạn chỉ cần nói tên (như ls), nó tự tìm; không có thì bạn phải nói đủ đường dẫn (như /bin/ls). Nhớ nhé: p = tìm hộ trong PATH, e = bạn tự đưa môi trường.

Definition. Only the e functions (execle, execve) let you specify the environment via envp; the rest pass on the calling process's existing environment (environ). Only the p functions (execlp, execvp) perform a PATH search; the others require a pathname to the exact file.13

execl ("/bin/ls","ls","-l",NULL);
execlp("ls","ls","-l",NULL); /* tìm PATH */
execle("/bin/ls","ls",NULL, envp);
execv ("/bin/ls", argv);
execvp("ls", argv);     /* tìm PATH */

execl ("/bin/ls","ls","-l",NULL);
execlp("ls",...); /* tự tìm trong PATH */
execv ("/bin/ls", argv);

execl ("/bin/ls","ls","-l",NULL);
execlp("ls","ls","-l",NULL); /* PATH search */
execle("/bin/ls","ls",NULL, envp);
execv ("/bin/ls", argv);
execvp("ls", argv);     /* PATH search */

  1. l vs v

    Danh sách hay vector — chọn theo lúc nào bạn biết các đối số Liệt kê từng cái hay đưa cả mảng List or vector — choose by when you know the arguments

    Các hàm chữ l (execl, execlp, execle) nhận đối số như một danh sách rời trong lời gọi, kết thúc bằng (char *) NULL — tiện khi bạn biết trước số và nội dung đối số lúc viết mã. Các hàm chữ v (execv, execvp, execvpe) nhận một mảng argv — tiện khi bạn phải dựng danh sách đối số lúc chạy.13

    Nếu lúc viết mã bạn đã biết chắc các tham số, hãy dùng hàm chữ l và liệt kê thẳng ra. Nếu đến lúc chạy mới biết (ví dụ do người dùng nhập), hãy gom chúng vào một mảng và dùng hàm chữ v. Nhớ nhé: biết sẵn thì liệt kê (l), chưa biết thì gom mảng (v).

    The l functions (execl, execlp, execle) take arguments as a loose list in the call, terminated by (char *) NULL — handy when you know the arguments at coding time. The v functions (execv, execvp, execvpe) take an argv array — handy when you must build the argument list at run time.13

    Bạn gặp nó ở đâuWhere you meet it Quên dấu (char *) NULL đóng danh sách của hàm chữ l là một lỗi kinh điển — hàm sẽ đọc tràn qua stack cho tới khi gặp một giá trị 0 ngẫu nhiên. Quên ô NULL cuối ở hàm chữ l là lỗi hay gặp — hàm không biết danh sách kết thúc ở đâu. Forgetting the terminating (char *) NULL on an l function is a classic bug — it reads past the stack until it hits a stray zero.
  2. PATH search (p)

    Chữ p: gọi tên trần, để hàm tự tìm trong PATH Chữ p: chỉ cần nói tên, nó tự tìm The p letter: name it bare, let the function search PATH

    execlp() và execvp() cho phép truyền một tên tệp trần thay vì đường dẫn đầy đủ: nếu tên không chứa dấu /, hàm lần lượt tìm trong các thư mục liệt kê bởi biến môi trường PATH. Nếu tên có chứa dấu gạch chéo thì PATH bị bỏ qua và tên được coi là đường dẫn trực tiếp.13

    Với hai hàm có chữ p, bạn chỉ cần viết ls chứ không cần /bin/ls; hàm sẽ dò qua các thư mục trong biến PATH để tìm. Nhưng nếu bạn viết sẵn có dấu / thì nó không tìm nữa mà dùng thẳng. Nhớ nhé: chữ p giúp bạn gọi lệnh như khi gõ trong terminal.

    execlp() and execvp() let you pass a bare filename instead of a full path: if the name contains no /, the function searches each directory listed in the PATH environment variable in turn. If the name does contain a slash, PATH is ignored and the name is taken as a direct path.13

    Bạn gặp nó ở đâuWhere you meet it Đây đúng là cơ chế mà shell dùng để tìm lệnh bạn gõ. Lưu ý bảo mật: trong chương trình đặc quyền, một PATH bị kẻ xấu điều khiển có thể khiến exec chạy nhầm chương trình — nên tránh dùng chữ p ở đó.1 Chính là cách terminal tìm lệnh. Nhưng trong chương trình quyền cao thì đừng dùng, vì PATH có thể bị người xấu sửa. This is exactly how a shell finds the command you type. Security note: in a privileged program, an attacker-controlled PATH could make exec run the wrong program — so avoid the p letter there.1
  3. environment (e)

    Chữ e: tự dựng môi trường cho chương trình mới Chữ e: tự trao danh sách biến môi trường The e letter: hand-build the new program's environment

    Mặc định, chương trình mới thừa hưởng bản sao môi trường (environ) của tiến trình gọi. Các hàm chữ e — execle() và chính execve() — cho bạn thay thế hoàn toàn bằng một mảng envp tự chọn, hữu ích khi cần cô lập hoặc bơm thêm biến môi trường cụ thể cho chương trình con.123

    Thường thì chương trình mới dùng lại các biến môi trường của cái gọi nó. Nhưng nếu muốn tự tay quyết định những biến đó, bạn dùng hàm có chữ e và đưa vào danh sách riêng. Nhớ nhé: chữ e cho bạn toàn quyền về môi trường mà chương trình con nhìn thấy.

    By default the new program inherits a copy of the calling process's environment (environ). The e functions — execle() and execve() itself — let you replace it entirely with a chosen envp array, useful when you need to isolate or inject specific environment variables for the child program.123

    Bạn gặp nó ở đâuWhere you meet it Khi một chương trình khởi chạy tiến trình con với môi trường "sạch" được kiểm soát chặt — ví dụ các trình giám sát dịch vụ dựng lại environ từ con số không. Khi một chương trình muốn con của nó chạy với bộ biến môi trường được dọn sạch, chọn lọc cẩn thận. When a program launches a child with a tightly controlled "clean" environment — e.g. service supervisors that rebuild environ from scratch.
  4. interpreter scripts (#!)

    Script thông dịch — khi tệp exec không phải mã máy Tệp bắt đầu bằng #! và cách exec chạy nó Interpreter scripts — when the exec'd file isn't machine code

    exec không chỉ chạy được tệp nhị phân. Nếu tệp bắt đầu bằng dòng #! interpreter [optional-arg], nhân sẽ thay vào đó chạy chương trình interpreter, truyền cho nó đường dẫn script làm đối số. Nhờ vậy một script shell hay Python có thể được exec như thể là chương trình thực thi.12

    Tệp đem exec không bắt buộc là mã máy. Nếu dòng đầu của tệp là #! kèm đường dẫn một "trình thông dịch" (như /bin/sh), nhân sẽ chạy trình đó và đưa tệp script cho nó đọc. Đó là lý do bạn chạy được một script như chạy một lệnh. Nhớ nhé: dòng #! ở đầu tệp nói cho hệ biết "hãy nhờ ai đọc tôi".

    exec can run more than binaries. If a file begins with the line #! interpreter [optional-arg], the kernel instead runs the interpreter program, passing it the script's path as an argument. This lets a shell or Python script be exec'd as though it were an executable.12

    Bạn gặp nó ở đâuWhere you meet it Mọi dòng #!/bin/sh hay #!/usr/bin/env python3 ở đầu script bạn từng viết — đó chính là cơ chế này. Mỗi dòng #!/bin/bash bạn thấy ở đầu một script chính là đang dùng tính năng này. Every #!/bin/sh or #!/usr/bin/env python3 atop a script you have written — that is this mechanism.

    Giới hạn độ dài dòng #!The #! line length limit

    Sách (2010, viết cho nhân Linux 2.6.x) cho biết Linux giới hạn dòng #! ở 127 ký tự; phần vượt quá bị cắt lặng lẽ.1 Lưu ý về nguồn: con số này đã đổi — kể từ Linux 5.1, giới hạn được nâng lên 255 ký tự.2 Ta theo con số của sách trong phần chính và ghi chú biến thể ở đây.The book (2010, written for Linux 2.6.x) notes that Linux limits the #! line to 127 characters; text beyond that is silently truncated.1 A note on sources: this number has changed — since Linux 5.1, the limit was raised to 255 characters.2 We follow the book's figure in the main text and record the variation here.

    Chỉ chữ p chạy được script trực tiếpOnly the p letter runs scripts bare-named

    Một tệp script phải có quyền thực thi, và trong các biến thể exec, execvp()/execlp() là những hàm xử lý được việc gọi một script không có dòng #! bằng cách tự chạy nó qua shell — một điểm tiện lợi riêng của nhánh chữ p.13A script file must be executable, and among the exec variants execvp()/execlp() are the ones that cope with running a script lacking a #! line by running it via the shell — a convenience peculiar to the p branch.13

Cụm khái niệm 03 Concept cluster 03

Qua exec: tệp đang mở sống sót, còn trình xử lý tín hiệu thì không Qua exec: tệp mở vẫn mở, cách xử lý tín hiệu bị xoá Across exec: open files survive, signal handlers do not

Theo mặc định, file descriptor vẫn mở qua một lần execve(), và sẵn sàng cho chương trình mới dùng.12 Đây là một đặc tính cố ý và hữu ích — nó cho phép shell thiết lập chuyển hướng I/O trước khi exec — nhưng cũng là một rủi ro: chương trình mới có thể vô tình thừa kế những descriptor nó chẳng biết gì. Tín hiệu lại khác: vì mã của các handler biến mất cùng chương trình cũ, mọi tín hiệu đang được bắt đều bị trả về xử lý mặc định.12

Khi exec, các tệp đang mở không tự đóng — chúng được trao sang cho chương trình mới. Điều đó thường có ích (ví dụ để bẻ dòng vào/ra), nhưng đôi khi lại là chỗ rò rỉ. Còn về tín hiệu: các "quy ước phản ứng" bạn từng cài sẽ mất, vì đoạn mã xử lý đã biến mất cùng chương trình cũ. Nhớ nhé: tệp mở thì theo sang, còn cách xử lý tín hiệu thì về vạch mặc định.

By default, file descriptors remain open across an execve(), ready for the new program to use.12 This is deliberate and useful — it lets a shell set up I/O redirection before exec — but it is also a hazard: the new program may unwittingly inherit descriptors it knows nothing about. Signals are different: because the handlers' code vanishes with the old program, any signals being caught are reset to their default disposition.12

Định nghĩa. Để một descriptor tự đóng khi exec thành công, ta bật cờ close-on-exec (FD_CLOEXEC) cho nó, thường bằng fcntl(fd, F_SETFD, FD_CLOEXEC) sau khi đọc cờ cũ bằng F_GETFD. Descriptor nào có cờ này sẽ bị nhân đóng tự động tại thời điểm exec; các descriptor khác vẫn mở.125

Nói đơn giản. Nếu bạn muốn một tệp tự đóng ngay khi exec, hãy dán cho nó cái nhãn "đóng-khi-exec" (FD_CLOEXEC). Nhân thấy nhãn đó thì đóng hộ. Không dán nhãn thì tệp vẫn mở sang chương trình mới. Nhớ nhé: muốn tệp không lọt sang chương trình mới, dán nhãn close-on-exec.

Definition. To make a descriptor close itself on a successful exec, set its close-on-exec flag (FD_CLOEXEC), typically with fcntl(fd, F_SETFD, FD_CLOEXEC) after reading the old flags with F_GETFD. A descriptor with this flag is closed automatically by the kernel at exec time; others remain open.125

int flags = fcntl(fd, F_GETFD);
if (flags == -1) errExit("fcntl");
flags |= FD_CLOEXEC;
fcntl(fd, F_SETFD, flags);
/* fd nay sẽ tự đóng khi exec */

flags = fcntl(fd, F_GETFD);
flags |= FD_CLOEXEC;
fcntl(fd, F_SETFD, flags);
/* tệp sẽ tự đóng khi exec */

int flags = fcntl(fd, F_GETFD);
if (flags == -1) errExit("fcntl");
flags |= FD_CLOEXEC;
fcntl(fd, F_SETFD, flags);
/* fd now closes itself on exec */

  1. FD_CLOEXEC

    Cờ close-on-exec — vì sao nó tồn tại Nhãn "đóng khi exec" dùng để làm gì The close-on-exec flag — why it exists

    Một chương trình thường mở nhiều tệp chỉ để dùng nội bộ — tệp log, socket, tệp tạm. Khi nó sắp exec thành một chương trình khác, các descriptor đó không nên lọt sang: chương trình mới chẳng biết chúng là gì, và việc giữ chúng mở có thể rò rỉ tài nguyên hoặc gây lỗ hổng bảo mật. Bật FD_CLOEXEC dặn nhân đóng chúng đúng lúc exec, một cách gọn và an toàn.15

    Chương trình hay mở tệp riêng để tự dùng. Khi nó biến thành chương trình khác, mấy tệp đó không nên theo sang — chương trình mới không cần và có thể bị lợi dụng. Nhãn FD_CLOEXEC nhờ nhân tự đóng chúng khi exec. Nhớ nhé: dán nhãn này cho mọi tệp bạn không muốn chia cho chương trình sau.

    A program often opens files purely for its own use — log files, sockets, temporary files. When it is about to exec into another program, those descriptors should not leak through: the new program knows nothing about them, and keeping them open can waste resources or open a security hole. Setting FD_CLOEXEC tells the kernel to close them precisely at exec, cleanly and safely.15

    Bạn gặp nó ở đâuWhere you meet it Khi một dịch vụ khởi chạy tiến trình con mà con đó lại "giữ mở" cổng mạng của cha, khiến cổng không giải phóng được — gần như luôn là vì thiếu FD_CLOEXEC.5 Khi một cổng mạng cứ bị "kẹt mở" sau khi chạy lệnh con — thường là vì quên dán nhãn đóng-khi-exec. When a service spawns a child that "holds open" the parent's network port so it cannot be freed — almost always a missing FD_CLOEXEC.5
  2. handlers reset

    Handler bị trả về mặc định — nhưng tín hiệu bị bỏ qua thì vẫn bị bỏ qua Cách xử lý tín hiệu bị xoá, trừ loại "mặc kệ" Handlers reset to default — but ignored signals stay ignored

    Mọi tín hiệu mà chương trình cũ đang bắt (có handler riêng) đều được đặt lại về xử lý mặc định sau exec, vì mã handler không còn tồn tại trong không gian địa chỉ mới. Ngược lại, các tín hiệu đang được bỏ qua (SIG_IGN) hay để ở mặc định thì giữ nguyên trạng thái đó.12

    Nếu chương trình cũ có cách phản ứng riêng với một tín hiệu, cách đó mất sau exec (quay về mặc định), vì đoạn mã phản ứng đã biến mất. Nhưng nếu nó chọn "mặc kệ" một tín hiệu, thì chương trình mới vẫn mặc kệ. Nhớ nhé: handler riêng thì mất, còn trạng thái "bỏ qua" thì theo sang.

    Any signal the old program was catching (with its own handler) is reset to its default disposition after exec, because the handler code no longer exists in the new address space. Signals being ignored (SIG_IGN) or left at default, by contrast, keep that disposition.12

    Bạn gặp nó ở đâuWhere you meet it Đây là lý do một chương trình mới được exec khởi đầu với phản ứng tín hiệu "sạch" — bạn không thể giả định nó thừa kế cách xử lý SIGINT, SIGTERM… của chương trình cha. Vì vậy chương trình mới luôn bắt đầu với cách phản ứng tín hiệu mặc định, không mang theo thói quen của chương trình cũ. This is why a freshly exec'd program starts with a "clean" signal setup — you cannot assume it inherits the parent's handling of SIGINT, SIGTERM, and so on.

    Ngoại lệ SIGCHLDThe SIGCHLD exception

    SUSv3 để không xác định việc một SIGCHLD đang bị bỏ qua có còn bị bỏ qua qua exec hay không; trên Linux, trạng thái bỏ qua được giữ lại — một điểm khác biệt đáng nhớ khi viết mã khả chuyển.12SUSv3 leaves it unspecified whether an ignored SIGCHLD remains ignored across exec; on Linux the ignore disposition is preserved — a notable portability wrinkle.12

    Mask và tín hiệu chờ thì sống sótThe mask and pending signals survive

    Khác với disposition, signal mask (tập tín hiệu đang bị chặn) và tập tín hiệu đang chờ (pending) của tiến trình được giữ nguyên qua exec — nên chương trình mới có thể khởi đầu với một số tín hiệu đã bị chặn sẵn mà không hay.12Unlike dispositions, the process's signal mask (the set of blocked signals) and set of pending signals are preserved across exec — so a new program may begin with some signals already blocked without realizing it.12

  3. what carries over

    Cái gì theo sang, cái gì không — một bảng ngắn để nhớ Tóm tắt: cái gì còn, cái gì mất qua exec What carries over and what doesn't — a short tally

    Nhìn tổng thể: sống sót qua exec gồm PID và quan hệ cha–con, các file descriptor không có FD_CLOEXEC, signal mask và tín hiệu chờ, thư mục làm việc, umask. Mất đi gồm text/data/heap/stack cũ (và mọi biến trong đó), các vùng nhớ ánh xạ, và các handler tín hiệu do chương trình cũ cài.12

    Nói gọn: giữ lại là số định danh, các tệp mở không dán nhãn đóng, cách chặn tín hiệu, thư mục đang đứng. Mất đi là toàn bộ bộ nhớ và cách phản ứng tín hiệu riêng của chương trình cũ. Nhớ nhé: cái thuộc "tiến trình" thì ở lại, cái thuộc "chương trình" thì đi.

    The big picture: what survives exec includes the PID and parent–child link, descriptors without FD_CLOEXEC, the signal mask and pending signals, the working directory, and the umask. What is lost includes the old text/data/heap/stack (and every variable in them), memory mappings, and the signal handlers the old program installed.12

    Bạn gặp nó ở đâuWhere you meet it Khi gỡ lỗi "vì sao chương trình con không thấy biến/handler của cha" — câu trả lời luôn nằm ở lằn ranh "thuộc tiến trình" so với "thuộc chương trình" này. Khi thắc mắc vì sao chương trình con "quên" mọi thứ của cha — chính là do ranh giới này. When debugging "why the child can't see the parent's variable/handler" — the answer always lies on this process-vs-program line.

Cụm khái niệm 04 Concept cluster 04

system(): tiện một dòng để chạy lệnh shell — và một mớ chi tiết để hiện thực đúng system(): một dòng chạy lệnh shell, nhưng viết nó cho đúng thì khó system(): a one-liner to run a shell command — and a thicket of detail to get right

Hàm system(command) cho phép một chương trình chạy một lệnh shell bất kỳ: nó khởi chạy /bin/sh với tùy chọn -c để thông dịch chuỗi command. Vì chạy qua shell, bạn được hưởng mọi tiện lợi của shell — ống (pipe), chuyển hướng, bung ký tự đại diện — mà không phải tự fork và exec.14

system("...") giống như bạn đưa cả một dòng lệnh cho terminal chạy hộ: nó gọi /bin/sh -c để thực thi. Nhờ vậy bạn dùng được pipe, chuyển hướng tệp… ngay trong chuỗi lệnh, mà không phải tự tay fork rồi exec. Nhớ nhé: system là lối tắt gọn nhất để nhờ shell chạy một dòng lệnh.

The system(command) function lets a program run an arbitrary shell command: it launches /bin/sh with the -c option to interpret the command string. Because it runs through a shell, you get all the shell's conveniences — pipes, redirection, wildcard expansion — without forking and exec'ing yourself.14

Định nghĩa. Giá trị trả về của system() được mã hóa như trạng thái của waitpid(), giải mã bằng WIFEXITED(), WEXITSTATUS()… Vài trường hợp đặc biệt: trả về −1 nếu không fork được hay không lấy được trạng thái con; nếu command là NULL thì trả về khác 0 khi có shell và 0 khi không; và nếu không chạy được shell thì trạng thái như thể shell đã exit(127).14

Nói đơn giản. Kết quả trả về cho biết lệnh chạy ra sao, đọc bằng các macro giống như khi đợi tiến trình con. Nếu đưa NULL, nó chỉ cho biết máy có shell hay không. Nếu không chạy nổi shell, mã trả về là 127. Nhớ nhé: nhớ kiểm tra giá trị trả về của system, đừng cho rằng lệnh luôn chạy được.

Definition. The return value of system() is encoded like a waitpid() status, decoded with WIFEXITED(), WEXITSTATUS(), etc. Special cases: it returns −1 if it could not fork or obtain the child's status; if command is NULL it returns nonzero when a shell is available and 0 otherwise; and if the shell cannot be executed the status is as though the shell called exit(127).14

int status = system("ls | wc -l");
if (status == -1) { /* fork/wait lỗi */ }
else if (WIFEXITED(status))
  printf("sh exit %d\n",
    WEXITSTATUS(status));

status = system("ls | wc -l");
if (status == -1) { /* hỏng */ }
/* đọc status bằng WIFEXITED... */

int status = system("ls | wc -l");
if (status == -1) { /* fork/wait error */ }
else if (WIFEXITED(status))
  printf("sh exit %d\n",
    WEXITSTATUS(status));

  1. convenience vs control

    Tiện lợi đánh đổi bằng tốc độ và an toàn Dễ dùng nhưng chậm hơn và cần cẩn thận Convenience traded for speed and safety

    system() đơn giản để dùng, nhưng cái giá là kém hiệu quả — mỗi lời gọi đòi ít nhất hai tiến trình (một cho shell, một hoặc nhiều cho lệnh) — và rủi ro an ninh nếu command chứa dữ liệu không tin cậy, vì shell sẽ diễn giải các ký tự đặc biệt. Khi cần kiểm soát hoặc tốc độ, hãy tự dùng fork() + exec().14

    Đổi lại sự tiện, system() chạy chậm hơn (phải bật thêm cả một shell) và dễ bị lợi dụng nếu bạn ghép chuỗi lệnh từ dữ liệu người dùng nhập. Khi cần nhanh hoặc an toàn, hãy tự fork và exec. Nhớ nhé: đừng nhét dữ liệu lạ vào chuỗi lệnh của system.

    system() is simple to use, but the cost is inefficiency — each call needs at least two processes (one for the shell, one or more for the command) — and a security risk if command contains untrusted data, since the shell interprets special characters. When you need control or speed, use fork() + exec() yourself.14

    Bạn gặp nó ở đâuWhere you meet it Lỗ hổng "command injection" kinh điển: ghép tên tệp do người dùng nhập thẳng vào chuỗi system() để kẻ xấu chèn ; rm -rf ~ — một lý do đủ để tránh system() với dữ liệu ngoài. Nhiều vụ tấn công xảy ra vì lập trình viên nhét tên tệp người dùng nhập thẳng vào system — tránh làm vậy. The classic "command injection" hole: splicing a user-supplied filename straight into a system() string lets an attacker inject ; rm -rf ~ — reason enough to avoid system() with external data.
  2. reading the status

    Đọc đúng giá trị trả về — nó không phải mã thoát của lệnh Giá trị trả về không phải mã thoát trực tiếp của lệnh Reading the status right — it isn't the command's exit code

    Một nhầm lẫn phổ biến là coi giá trị trả về của system() như mã thoát của lệnh. Thực ra nó là trạng thái kết thúc của shell chạy lệnh, mã hóa theo kiểu waitpid() — nên phải giải mã bằng các macro WIFEXITED()/WEXITSTATUS(), và bản thân shell cũng có thể báo lỗi của riêng nó (ví dụ exit 127 khi không tìm thấy lệnh).14

    Đừng tưởng số system() trả về chính là mã thoát của lệnh. Nó là trạng thái của shell, cần giải mã bằng các macro đã học khi đợi tiến trình con. Và chính shell cũng có thể trả 127 nếu không tìm thấy lệnh. Nhớ nhé: luôn giải mã status bằng WIFEXITED/WEXITSTATUS.

    A common confusion is to treat system()'s return value as the command's exit code. It is actually the termination status of the shell that ran the command, encoded in waitpid() form — so you must decode it with WIFEXITED()/WEXITSTATUS(), and the shell itself may report its own error (e.g. exit 127 when the command is not found).14

    Bạn gặp nó ở đâuWhere you meet it Khi mã của bạn tưởng lệnh "thành công" chỉ vì system() trả về khác 0 nhưng thực ra 0 mới là lệnh thoát sạch — đọc nhầm status là bẫy hay gặp. Khi bạn hiểu ngược kết quả thành công/thất bại vì không giải mã status đúng cách. When your code thinks a command "succeeded" because system() returned nonzero, when a clean exit is actually 0 — misreading the status is a frequent trap.
  3. implementing system()

    Hiện thực system() cho đúng — phần tín hiệu mới là chỗ khó Tự viết system(): khó nhất là xử lý tín hiệu Implementing system() correctly — the signals are the hard part

    Về cơ bản, system() là fork(), rồi con execl("/bin/sh", "sh", "-c", command, (char *) NULL) (và gọi _exit(127) nếu exec hỏng), còn cha waitpid() đợi con. Nhưng một hiện thực đúng (Listing 27-9 của sách, trang 586) còn phải xử lý tín hiệu cẩn thận: trong lúc lệnh chạy, cha phải bỏ qua SIGINT và SIGQUIT và chặn SIGCHLD, rồi khôi phục lại khi xong.146

    Ruột của system() chỉ là: tách một con, cho con chạy /bin/sh -c "lệnh", cha đợi con xong. Nhưng để chạy mượt trong mọi tình huống, cha còn phải tạm thời "mặc kệ" vài tín hiệu (Ctrl-C, Ctrl-\) và chặn tín hiệu báo con chết, rồi trả lại như cũ sau đó. Nhớ nhé: phần khó của system không phải fork/exec, mà là dọn dẹp tín hiệu cho đúng.

    At its core, system() is a fork(), then the child does execl("/bin/sh", "sh", "-c", command, (char *) NULL) (calling _exit(127) if exec fails), while the parent waitpid()s for it. But a correct implementation (the book's Listing 27-9, p. 586) must also handle signals carefully: while the command runs, the parent must ignore SIGINT and SIGQUIT and block SIGCHLD, then restore them afterward.146

    Bạn gặp nó ở đâuWhere you meet it Mỗi khi bạn nhấn Ctrl-C trong lúc một chương trình đang chạy lệnh con qua system(): nhờ cách bẫy tín hiệu này mà tín hiệu tới đúng lệnh con chứ không giết oan chương trình cha. Khi bạn bấm Ctrl-C lúc một lệnh con đang chạy: nhờ xử lý tín hiệu đúng mà chỉ lệnh con dừng, không phải cả chương trình. Every time you press Ctrl-C while a program runs a child command via system(): this signal handling is why the signal reaches the command, not the unlucky parent.

    Vì sao bỏ qua SIGINT/SIGQUITWhy ignore SIGINT/SIGQUIT

    Theo SUSv3, trong lúc lệnh của system() chạy, SIGINT và SIGQUIT nên được bỏ qua ở tiến trình gọi, còn SIGCHLD nên bị chặn. Lý do: lệnh con đang là "tiền cảnh" của terminal, nên khi người dùng gõ Ctrl-C thì tín hiệu phải tác động lên lệnh đó, không phải lên chương trình đã gọi system().14Per SUSv3, while the system() command runs, SIGINT and SIGQUIT should be ignored in the calling process and SIGCHLD should be blocked. The reason: the child command is the terminal's foreground job, so a user's Ctrl-C should act on that command, not on the program that called system().14

    Vì sao chặn SIGCHLDWhy block SIGCHLD

    Chặn SIGCHLD quanh vùng fork/wait tránh trường hợp một handler SIGCHLD của chương trình gọi "gặt" mất tiến trình con của system() trước khi system() kịp waitpid() lấy trạng thái — một lỗi tương tranh tinh vi mà hiện thực đúng phải phòng.16Blocking SIGCHLD around the fork/wait region avoids a case where the caller's own SIGCHLD handler "reaps" the system() child before system() can waitpid() for its status — a subtle race a correct implementation must guard against.16

Ba ngộ nhận thường gặp Three common misconceptions

“exec() tạo một tiến trình mới” “exec() creates a new process”

Ngộ nhận: gọi execve() sẽ sinh thêm một tiến trình chạy chương trình mới.

Thực tế: exec thay chương trình trong tiến trình hiện tại và giữ nguyên PID; muốn có tiến trình mới thì phải fork() trước.12

Myth: calling execve() spawns an additional process running the new program.

Reality: exec replaces the program in the current process and keeps the PID; to get a new process you must fork() first.12

“Hàm exec() nào cũng là system call và cũng tìm PATH” “Every exec() function is a system call and searches PATH”

Ngộ nhận: cả sáu hàm đều như nhau, đều nhờ nhân và đều tự tìm chương trình.

Thực tế: chỉ execve() là system call; chỉ các hàm chữ p (execlp, execvp) tìm PATH; chỉ chữ e cho truyền môi trường riêng.13

Myth: all six functions are alike — all trap into the kernel and all find the program.

Reality: only execve() is a system call; only the p functions (execlp, execvp) search PATH; only the e ones take a custom environment.13

“exec tự đóng các tệp đang mở của tôi” “exec closes my open files for me”

Ngộ nhận: sau exec, các file descriptor cũ biến mất một cách an toàn.

Thực tế: mặc định chúng vẫn mở và lọt sang chương trình mới; muốn đóng tự động phải bật FD_CLOEXEC bằng fcntl().125

Myth: after exec, old file descriptors are safely gone.

Reality: by default they stay open and leak into the new program; to close them automatically you must set FD_CLOEXEC with fcntl().125

Tự kiểm Self-check

Năm câu hỏi để biết bạn đã nắm cách thực thi chương trình Năm câu hỏi kiểm tra nhanh Five questions to check you grasped program execution

Trả lời được những câu này nghĩa là bạn đã nắm cơ chế exec và cách nó ghép với fork/wait thành bộ ba nền tảng của UNIX.

Nếu trả lời được, nghĩa là bạn đã hiểu chương này.

Answering these means you have the exec mechanism and how it joins fork/wait into the foundational UNIX trio.

  1. Vì sao một lời gọi execve() thành công lại "không bao giờ trở về", và điều đó khác fork() ở điểm căn bản nào?

    Gợi ý sâu hơn: sau exec, những thuộc tính nào của tiến trình sống sót, những gì bị mất?

  2. Giải thích các chữ cái l, v, e, p trong tên các hàm exec*(). Hàm nào tìm PATH, hàm nào cho truyền môi trường riêng?

    Gợi ý sâu hơn: khi nào nên dùng hàm chữ l, khi nào nên dùng hàm chữ v?

  3. Dòng #! ở đầu một tệp script làm gì khi tệp đó bị exec? Nêu một giới hạn mà Linux đặt lên dòng này.

    Gợi ý sâu hơn: con số giới hạn trong sách là bao nhiêu, và nó đã thay đổi ra sao từ Linux 5.1?

  4. Qua một lần exec, điều gì xảy ra với các file descriptor đang mở và với các trình xử lý tín hiệu? Cờ FD_CLOEXEC thay đổi điều gì?

    Gợi ý sâu hơn: signal mask và tín hiệu đang chờ có sống sót qua exec không? SIGCHLD đặc biệt ở chỗ nào?

  5. system() làm gì bên dưới, và vì sao một hiện thực đúng lại phải bỏ qua SIGINT/SIGQUIT và chặn SIGCHLD?

    Gợi ý sâu hơn: giá trị trả về của system() được mã hóa thế nào, và giải mã bằng macro nào?

  1. exec khác fork ở chỗ nào? Cái nào tạo tiến trình mới, cái nào thay chương trình?

  2. Các chữ l, v, e, p trong tên hàm exec nói lên điều gì?

  3. Dòng #! ở đầu một tệp script dùng để làm gì?

  4. Sau khi exec, các tệp đang mở còn mở không? Làm sao để một tệp tự đóng khi exec?

  5. system() dùng để làm gì, và vì sao nên cẩn thận khi chuỗi lệnh chứa dữ liệu người dùng?

  1. Why does a successful execve() "never return", and how does that differ fundamentally from fork()?

  2. Explain the letters l, v, e, p in the exec*() names. Which search PATH, which take a custom environment?

  3. What does a #! line at the top of a script do when the file is exec'd? Name one limit Linux places on it.

  4. Across an exec, what happens to open file descriptors and to signal handlers? What does FD_CLOEXEC change?

  5. What does system() do underneath, and why must a correct implementation ignore SIGINT/SIGQUIT and block SIGCHLD?

Khép lại chương Closing the chapter

Thực thi chương trình gói gọn trong một ý: execve() thay chương trình đang chạy chứ không tạo tiến trình mới, nên ghép với fork nó làm nên gần như mọi thứ trong UNIX. Các biến thể exec*() chỉ khác ở cách truyền đối số, PATH và môi trường; qua exec tệp đang mở sống sót, còn handler tín hiệu thì về mặc định; và system() gói trọn khuôn mẫu đó — tiện, nhưng chỉ đúng khi tín hiệu được xử lý cẩn thận.1

Tóm lại: exec thay chương trình, giữ nguyên tiến trình; ghép với fork là chạy được mọi lệnh. Qua exec, tệp mở theo sang, cách xử lý tín hiệu thì về mặc định. Nhớ nhé: fork để tách ra, exec để biến thành chương trình mới, wait để đợi — đó là nhịp của cả UNIX.

Program execution comes down to one idea: execve() replaces the running program rather than creating a new process, so paired with fork it builds almost everything in UNIX. The exec*() variants differ only in argument passing, PATH, and environment; across exec open files survive while signal handlers reset to default; and system() wraps that whole pattern — convenient, but correct only when signals are handled with care.1