Tại sao vỏ gọi fork ()?


32

Khi một tiến trình được bắt đầu từ một shell, tại sao shell lại tự phân tách trước khi thực hiện tiến trình?

Ví dụ: khi người dùng nhập grep blabla foo, tại sao shell không thể gọi exec()trên grep mà không có shell con?

Ngoài ra, khi shell tự tạo trong trình giả lập thiết bị đầu cuối GUI, nó có khởi động trình giả lập thiết bị đầu cuối khác không? (chẳng hạn như pts/13bắt đầu pts/14)

Câu trả lời:


34

Khi bạn gọi một execphương thức gia đình, nó không tạo ra một quy trình mới, thay vào đó execthay thế bộ nhớ quy trình hiện tại và tập lệnh, v.v. bằng quy trình bạn muốn chạy.

Ví dụ, bạn muốn chạy grepbằng exec. bashlà một quá trình (có bộ nhớ riêng, không gian địa chỉ). Bây giờ khi bạn gọi exec(grep), exec sẽ thay thế bộ nhớ hiện tại, không gian địa chỉ, tập lệnh vv bằng grep'sdữ liệu. Điều đó có nghĩa là bashquá trình sẽ không còn tồn tại. Kết quả là bạn không thể quay lại terminal sau khi hoàn thành greplệnh. Đó là lý do tại sao các phương thức gia đình thực hiện không bao giờ quay trở lại. Bạn không thể thực thi bất kỳ mã nào sau khi thực hiện; nó là không thể truy cập.


Hầu như ok --- Tôi thay thế Terminal bằng bash. ;-)
Rmano

2
BTW, bạn có thể yêu cầu bash thực thi grep mà không cần chuyển trước, bằng cách sử dụng lệnh exec grep blabla foo. Tất nhiên, trong trường hợp cụ thể này, nó sẽ không hữu ích (vì cửa sổ đầu cuối của bạn sẽ đóng ngay khi grep kết thúc), nhưng đôi khi nó có thể tiện dụng (ví dụ: nếu bạn đang khởi động một shell khác, có thể thông qua ssh / sudo / screen và không có ý định quay lại bản gốc hoặc nếu tiến trình shell mà bạn đang chạy này là shell phụ không bao giờ có nghĩa là thực thi nhiều hơn một lệnh).
Ilmari Karonen

7
Tập lệnh có ý nghĩa rất cụ thể. Và đó không phải là ý nghĩa mà bạn đang sử dụng.
Andrew Savinykh

@IlmariKaronen Nó sẽ hữu ích trong các tập lệnh bao bọc, nơi bạn muốn chuẩn bị các đối số và môi trường cho một lệnh. Và trường hợp bạn đã đề cập, trong đó bash không bao giờ có nghĩa là chạy nhiều hơn một lệnh, đó thực sự là bash -c 'grep foo bar'và gọi exec có dạng bash tối ưu hóa tự động cho bạn
Sergiy Kolodyazhnyy

3

Theo pts, hãy tự kiểm tra: trong một cái vỏ, chạy

echo $$ 

để biết id quá trình của bạn (PID), tôi có ví dụ

echo $$
29296

Sau đó chạy ví dụ sleep 60và sau đó, trong một thiết bị đầu cuối khác

(0)samsung-romano:~% ps -edao pid,ppid,tty,command | grep 29296 | grep -v grep
29296  2343 pts/11   zsh
29499 29296 pts/11   sleep 60

Vì vậy, không, nói chung bạn có cùng tty liên quan đến quá trình. (Lưu ý rằng đây là của bạn sleepvì nó có vỏ của bạn là cha mẹ).


2

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()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 -hchạ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 stdoutaka 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 dfxảy ra, quá trình con sẽ không nghĩ gì về nó stdoutvà 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 grepvớ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ừ sourcelệ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


1

Đối với mỗi lệnh (ví dụ: grep) mà bạn phát hành trên dấu nhắc bash, bạn thực sự có ý định bắt đầu một quy trình mới và sau đó quay lại dấu nhắc bash sau khi thực hiện.

Nếu tiến trình shell (bash) gọi exec () để chạy grep, tiến trình shell sẽ được thay thế bằng grep. Grep sẽ hoạt động tốt nhưng sau khi thực thi, điều khiển không thể quay lại shell vì quá trình bash đã được thay thế.

Vì lý do này, bash gọi fork (), không thay thế quy trình hiện tại.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.