TL; DR : Bởi vì đây là phương pháp tối ưu để tạo các quy trình mới và giữ quyền kiểm soát trong vỏ tương tác
fork () là cần thiết cho các quy trình và đường ống
Để trả lời phần cụ thể của câu hỏi này, nếu grep blabla foo
được gọi exec()
trực tiếp qua cha mẹ, cha mẹ sẽ nắm bắt để tồn tại và PID của nó với tất cả các tài nguyên sẽ được tiếp quản grep blabla foo
.
Tuy nhiên, hãy nói chung chung về exec()
và fork()
. Lý do chính cho hành vi đó là vì fork()/exec()
phương pháp tiêu chuẩn để tạo ra một quy trình mới trên Unix / Linux và đây không phải là một điều cụ thể bash; phương pháp này đã được áp dụng ngay từ đầu và bị ảnh hưởng bởi cùng phương thức này từ các hệ điều hành đã có từ thời đó. Để phần nào diễn giải câu trả lời của goldilocks cho một câu hỏi liên quan, fork()
để tạo quy trình mới dễ dàng hơn vì hạt nhân có ít công việc phải phân bổ tài nguyên hơn, và rất nhiều thuộc tính (như mô tả tệp, môi trường, v.v.) - tất cả đều có thể được kế thừa từ quá trình cha mẹ (trong trường hợp này là từ bash
).
Thứ hai, theo như các shell tương tác, bạn không thể chạy một lệnh bên ngoài mà không cần gạt. Để khởi chạy một tệp thực thi sống trên đĩa (ví dụ /bin/df -h
:), bạn phải gọi một trong các exec()
hàm gia đình, chẳng hạn như execve()
, sẽ thay thế cha mẹ bằng quy trình mới, tiếp quản bộ mô tả tệp PID và tệp hiện có, v.v. Đối với shell tương tác, bạn muốn điều khiển quay trở lại người dùng và để shell tương tác cha mẹ tiếp tục. Vì vậy, cách tốt nhất là tạo ra một quy trình con thông qua fork()
và để cho quá trình đó được thực hiện thông qua execve()
. Vì vậy, shell tương tác PID 1156 sẽ sinh ra một đứa trẻ thông qua fork()
PID 1157, sau đó gọi execve("/bin/df",["df","-h"],&environment)
, nó sẽ /bin/df -h
chạy với PID 1157. Bây giờ, shell chỉ phải chờ quá trình thoát ra và trả lại quyền điều khiển cho nó.
Trong trường hợp bạn phải tạo một đường ống giữa hai hoặc nhiều lệnh, giả sử df | grep
, bạn cần một cách để tạo hai mô tả tệp (đó là đọc và ghi kết thúc của đường ống xuất phát từ tòa nhà pipe()
chọc trời), sau đó bằng cách nào đó hãy để hai quy trình mới kế thừa chúng. Điều đó đã được thực hiện trong quá trình xử lý mới và sau đó bằng cách sao chép đầu ghi của đường ống thông qua dup2()
cuộc gọi vào stdout
aka fd 1 của nó (vì vậy nếu kết thúc ghi là fd 4, chúng tôi sẽ thực hiện dup2(4,1)
). Khi exec()
sinh sản df
xảy ra, quá trình con sẽ không nghĩ gì về nó stdout
và viết cho nó mà không nhận thức được (trừ khi nó chủ động kiểm tra) rằng đầu ra của nó thực sự đi theo đường ống. Quá trình tương tự xảy ra với grep
, ngoại trừ chúng tôi fork()
, hãy đọc kết thúc đường ống với fd 3 và dup(3,0)
trước khi sinh sản grep
vớiexec()
. Tất cả quá trình cha mẹ thời gian này vẫn còn đó, chờ đợi để lấy lại quyền kiểm soát khi đường ống hoàn thành.
Trong trường hợp các lệnh tích hợp, thường thì shell không có fork()
, ngoại trừ source
lệnh. Subshells yêu cầu fork()
.
Tóm lại, đây là một cơ chế cần thiết và hữu ích.
Nhược điểm của việc rèn và tối ưu hóa
Bây giờ, điều này là khác nhau cho các vỏ không tương tác , chẳng hạn như bash -c '<simple command>'
. Mặc dù fork()/exec()
là phương pháp tối ưu khi bạn phải xử lý nhiều lệnh, nhưng thật lãng phí tài nguyên khi bạn chỉ có một lệnh duy nhất. Để trích dẫn Stéphane Chazelas từ bài đăng này :
Ngã ba rất tốn kém, về thời gian CPU, bộ nhớ, bộ mô tả tệp được phân bổ ... Có một quy trình shell nằm chờ đợi một quy trình khác trước khi thoát là một sự lãng phí tài nguyên. Ngoài ra, nó gây khó khăn cho việc báo cáo chính xác trạng thái thoát của quy trình riêng biệt sẽ thực thi lệnh (ví dụ: khi quy trình bị hủy).
Do đó, nhiều shell (không chỉ bash
) sử dụng exec()
để cho phép điều đó bash -c ''
được thực hiện bằng lệnh đơn giản đó. Và chính xác cho các lý do đã nêu ở trên, giảm thiểu các đường ống trong các kịch bản shell là tốt hơn. Thường thì bạn có thể thấy những người mới bắt đầu làm một cái gì đó như thế này:
cat /etc/passwd | cut -d ':' -f 6 | grep '/home'
Tất nhiên, điều này sẽ fork()
3 quá trình. Đây là một ví dụ đơn giản, nhưng hãy xem xét một tệp lớn, trong phạm vi Gigabyte. Nó sẽ hiệu quả hơn nhiều với một quy trình:
awk -F':' '$6~"/home"{print $6}' /etc/passwd
Lãng phí tài nguyên thực sự có thể là một hình thức tấn công từ chối dịch vụ và đặc biệt là bom ngã ba được tạo ra thông qua các chức năng vỏ tự gọi mình trong đường ống, tạo ra nhiều bản sao của chính chúng. Ngày nay, điều này được giảm thiểu thông qua việc giới hạn số lượng quá trình tối đa trong các nhóm trên systemd , mà Ubuntu cũng sử dụng kể từ phiên bản 15.04.
Tất nhiên điều đó không có nghĩa là rèn chỉ là xấu. Đây vẫn là một cơ chế hữu ích như đã thảo luận trước đây, nhưng trong trường hợp bạn có thể thoát khỏi ít quy trình hơn và liên tục ít tài nguyên hơn và do đó hiệu suất tốt hơn, thì bạn nên tránh fork()
nếu có thể.
Xem thêm