Tại sao chúng ta cần rẽ nhánh để tạo ra các quy trình mới?


95

Trong Unix bất cứ khi nào chúng tôi muốn tạo một quy trình mới, chúng tôi chia rẽ quy trình hiện tại, tạo một quy trình con mới giống hệt như quy trình cha mẹ; sau đó chúng tôi thực hiện một cuộc gọi hệ thống exec để thay thế tất cả dữ liệu từ quy trình cha mẹ bằng quy trình đó cho quy trình mới.

Tại sao chúng ta tạo một bản sao của quy trình cha mẹ ngay từ đầu và không trực tiếp tạo quy trình mới?


Câu trả lời:


61

Câu trả lời ngắn gọn là, forktrong Unix vì nó dễ phù hợp với hệ thống hiện có vào thời điểm đó và bởi vì một hệ thống tiền thân tại Berkeley đã sử dụng khái niệm dĩa.

Từ Sự phát triển của Hệ thống chia sẻ thời gian Unix (văn bản có liên quan đã được tô sáng ):

Kiểm soát quá trình ở dạng hiện đại của nó đã được thiết kế và thực hiện trong vài ngày. Thật đáng ngạc nhiên khi nó dễ dàng lắp vào hệ thống hiện có; đồng thời dễ dàng nhận thấy một số tính năng hơi khác thường của thiết kế hiện diện chính xác vì chúng thể hiện những thay đổi nhỏ, dễ mã hóa với những gì tồn tại . Một ví dụ điển hình là sự phân tách của các hàm fork và exec. Mô hình phổ biến nhất để tạo ra các quy trình mới liên quan đến việc chỉ định một chương trình cho quy trình thực hiện; trong Unix, một tiến trình rẽ nhánh tiếp tục chạy cùng một chương trình với cha mẹ của nó cho đến khi nó thực hiện một lệnh thực thi rõ ràng. Sự phân tách các chức năng chắc chắn không phải là duy nhất đối với Unix và trên thực tế, nó đã có mặt trong hệ thống chia sẻ thời gian của Berkeley, vốn nổi tiếng với Thompson. Tuy nhiên, có vẻ hợp lý khi cho rằng nó tồn tại trong Unix chủ yếu là do việc dễ dàng thực hiện fork mà không cần thay đổi nhiều thứ khác . Hệ thống đã xử lý nhiều quá trình (tức là hai); có một bảng quy trình và các quy trình được hoán đổi giữa bộ nhớ chính và đĩa. Việc thực hiện ban đầu của ngã ba chỉ yêu cầu

1) Mở rộng bảng quy trình

2) Bổ sung một cuộc gọi rẽ nhánh đã sao chép quy trình hiện tại vào khu vực trao đổi đĩa, sử dụng các nguyên hàm IO trao đổi hiện có và thực hiện một số điều chỉnh cho bảng quy trình.

Trên thực tế, cuộc gọi rẽ nhánh của PDP-7 yêu cầu chính xác 27 dòng mã lắp ráp. Tất nhiên, những thay đổi khác trong hệ điều hành và chương trình người dùng là bắt buộc, và một số trong số chúng khá thú vị và bất ngờ. Nhưng một trình điều khiển kết hợp sẽ phức tạp hơn đáng kể , nếu chỉ vì thực thi như vậy không tồn tại; chức năng của nó đã được thực hiện, sử dụng IO rõ ràng, bằng shell.

Kể từ bài báo đó, Unix đã phát triển. forktheo sau execkhông còn là cách duy nhất để chạy một chương trình.

  • vfork được tạo ra để trở thành một ngã ba hiệu quả hơn cho trường hợp quy trình mới dự định thực hiện một lệnh thực thi ngay sau ngã ba. Sau khi thực hiện một vfork, các tiến trình cha và con chia sẻ cùng một không gian dữ liệu và tiến trình cha mẹ bị đình chỉ cho đến khi tiến trình con thực hiện chương trình hoặc thoát.

  • posix_spawn tạo ra một quy trình mới và thực thi một tệp trong một cuộc gọi hệ thống duy nhất. Phải mất một loạt các tham số cho phép bạn chia sẻ có chọn lọc các tệp đang mở của người gọi và sao chép bố trí tín hiệu của nó và các thuộc tính khác vào quy trình mới.


5
Câu trả lời hay nhưng tôi sẽ thêm rằng vfork không nên được sử dụng nữa. Sự khác biệt hiệu suất là cận biên và việc sử dụng nó có thể nguy hiểm. Xem câu hỏi SO stackoverflow.com/questions/4856255/, trang này ewontfix.com/7 và "Lập trình Unix nâng cao" trang 299 về vfork
Raphael Ahrens

4
Các mưu mô (thiết lập cấu trúc dữ liệu) được yêu cầu sử dụng posix_spawn()để thực hiện cùng một công việc thay thế sau ngã ba có thể được thực hiện dễ dàng bằng cách sử dụng fork()và mã nội tuyến tạo ra một đối số hấp dẫn fork()để sử dụng đơn giản hơn nhiều.
Jonathan Leffler

34

[Tôi sẽ lặp lại một phần câu trả lời của tôi từ đây .]

Tại sao không chỉ có một lệnh tạo ra một quy trình mới từ đầu? Không phải là vô lý và không hiệu quả để sao chép một cái mà sẽ chỉ được thay thế ngay lập tức?

Trên thực tế, điều đó có thể sẽ không hiệu quả vì một vài lý do:

  1. "Bản sao" được tạo bởi fork()một chút trừu tượng, vì kernel sử dụng hệ thống sao chép trên ghi ; tất cả những gì thực sự phải được tạo ra là một bản đồ bộ nhớ ảo. Nếu bản sao ngay lập tức gọi exec(), hầu hết dữ liệu sẽ được sao chép nếu nó bị sửa đổi bởi hoạt động của quy trình thực sự không bao giờ phải sao chép / tạo vì quy trình không làm bất cứ điều gì cần sử dụng.

  2. Các khía cạnh quan trọng khác nhau của quy trình con (ví dụ, môi trường của nó) không phải được sao chép hoặc đặt riêng lẻ dựa trên phân tích phức tạp của bối cảnh, v.v. Chúng chỉ được coi là giống như quy trình gọi và đây là hệ thống khá trực quan mà chúng ta quen thuộc.

Để giải thích thêm 1 chút nữa, bộ nhớ được "sao chép" nhưng không bao giờ được truy cập sau đó không bao giờ thực sự được sao chép, ít nhất là trong hầu hết các trường hợp. Một ngoại lệ trong ngữ cảnh này có thể là nếu bạn rẽ nhánh một quy trình, sau đó có lối thoát quy trình cha mẹ trước khi đứa trẻ tự thay thế exec(). Tôi nói có thể bởi vì phần lớn cha mẹ có thể được lưu trong bộ nhớ cache nếu có đủ bộ nhớ trống và tôi không chắc mức độ này sẽ được khai thác ở mức độ nào (điều này phụ thuộc vào việc triển khai HĐH).

Tất nhiên, điều đó không có trên làm cho bề mặt sử dụng một bản sao hơn hiệu quả hơn là sử dụng một phiến đá trắng - ngoại trừ "những phiến đá trắng" không phải là nghĩa đen không có gì, và phải liên quan đến phân bổ. Hệ thống này có thể có một generic trống / mới quy trình mẫu mà nó sao chép cùng một cách, 1 nhưng điều đó sẽ sau đó không thực sự tiết kiệm bất cứ điều gì so với ngã ba copy-on-write. Vì vậy, # 1 chỉ chứng minh rằng sử dụng quy trình trống "mới" sẽ không hiệu quả hơn.

Điểm # 2 giải thích tại sao sử dụng ngã ba có khả năng hiệu quả hơn. Môi trường của một đứa trẻ được thừa hưởng từ cha mẹ của nó, ngay cả khi đó là một thực thi hoàn toàn khác. Ví dụ: nếu tiến trình cha là shell và trình duyệt web con $HOMEvẫn giống nhau cho cả hai, nhưng vì sau đó có thể thay đổi nó, đây phải là hai bản sao riêng biệt. Một trong những đứa trẻ được sản xuất bởi bản gốc fork().

1. Một chiến lược có thể không có nhiều ý nghĩa theo nghĩa đen, nhưng quan điểm của tôi là việc tạo ra một quy trình liên quan đến nhiều hơn là sao chép hình ảnh của nó vào bộ nhớ từ đĩa.


3
Mặc dù cả hai điểm đều đúng, nhưng không hỗ trợ tại sao phương pháp forking được chọn thay vì làm hỏng một quy trình mới từ thực thi được đưa ra.
SkyDan

3
Tôi nghĩ rằng điều này không trả lời câu hỏi. Fork được sử dụng bởi vì, trong trường hợp tạo ra một quy trình mới là cách hiệu quả nhất, chi phí sử dụng fork thay vì không đáng kể (có thể ít hơn 1% chi phí tạo quy trình). Mặt khác, có nhiều nơi mà fork có hiệu quả cao hơn hoặc đơn giản hơn rất nhiều về API (chẳng hạn như xử lý các tệp xử lý tệp). Quyết định mà Unix đưa ra là chỉ hỗ trợ một API, làm cho đặc tả kỹ thuật đơn giản hơn.
Cort Ammon

1
@SkyDan Bạn nói đúng, đó là câu trả lời cho lý do tại sao không phải là lý do tại sao , mà Mark Plotnick trả lời trực tiếp hơn - mà tôi sẽ giải thích không chỉ là đây là lựa chọn dễ nhất, mà còn có lẽ là hiệu quả nhất sự lựa chọn (theo trích dẫn của Dennis Richie: "cuộc gọi ngã ba của PDP-7 yêu cầu chính xác 27 dòng lắp ráp ... thực thi như vậy không tồn tại; chức năng của nó đã được thực hiện"). Vì vậy, "tại sao không" thực sự là một suy nghĩ về hai chiến lược trong đó một chiến lược bề ngoài có vẻ đơn giản và hiệu quả hơn, khi có lẽ nó không (chứng kiến ​​số phận đáng ngờ của ...
goldilocks

1
Goldilocks là chính xác. Có những tình huống mà việc rèn và sửa đổi rẻ hơn so với việc tạo một cái mới từ đầu. Tất nhiên, ví dụ cực đoan nhất là bất cứ lúc nào bạn muốn có một hành vi ngã ba. fork()nó có thể rất nhanh chóng (như GL đã đề cập, theo thứ tự 27 dòng lắp ráp). Nhìn theo một hướng khác, nếu bạn muốn "tạo một quy trình từ đầu", fork()chi phí chỉ cao hơn một chút so với bắt đầu từ quy trình được tạo trống (27 dòng lắp ráp + chi phí xử lý tệp đóng). Vì vậy, forkxử lý cả ngã ba và tạo tốt, trong khi createchỉ có thể xử lý tạo tốt.
Cort Ammon

2
Câu trả lời của bạn đề cập đến các cải tiến phần cứng: bộ nhớ ảo, sao chép khi ghi. Trước đó, forkthực sự sao chép tất cả bộ nhớ quá trình, và nó rất tốn kém.
Barmar

6

Tôi nghĩ lý do Unix chỉ có forkchức năng tạo ra các quy trình mới là kết quả của triết lý Unix

Họ xây dựng một chức năng làm một việc tốt. Nó tạo ra một quá trình con.

Những gì người ta làm với quy trình mới là tùy thuộc vào lập trình viên. Anh ta có thể sử dụng một trong các exec*chức năng và bắt đầu một chương trình khác, hoặc anh ta không thể sử dụng exec và sử dụng hai phiên bản của cùng một chương trình, điều này có thể hữu ích.

Vì vậy, bạn có được một mức độ tự do lớn hơn kể từ khi bạn có thể sử dụng

  1. ngã ba mà không thực hiện *
  2. ngã ba với exec * hoặc
  3. chỉ cần thực hiện * mà không cần ngã ba

và ngoài ra, bạn chỉ phải ghi nhớ các cuộc gọi forkexec*chức năng, điều mà trong những năm 1970 bạn phải làm.


3
Tôi hiểu cách thức hoạt động của dĩa và cách sử dụng chúng. Nhưng tại sao tôi muốn tạo ra một quy trình mới, khi tôi có thể làm điều tương tự nhưng với ít nỗ lực hơn? Ví dụ, giáo viên của tôi đã giao cho tôi một bài tập mà tôi phải tạo một quy trình cho mỗi số được truyền cho argv, để kiểm tra xem số đó có phải là số nguyên tố hay không. Nhưng đó không phải là một đường vòng cuối cùng làm điều tương tự sao? Tôi chỉ có thể sử dụng một mảng và sử dụng một hàm cho mỗi số ... Vậy tại sao chúng ta tạo các tiến trình con, thay vì thực hiện tất cả các xử lý trong quy trình chính?
dùng1534664

2
Tôi muốn nói rằng bạn hiểu cách thức hoạt động của dĩa và cách sử dụng chúng, bởi vì bạn đã từng có một giáo viên giao nhiệm vụ cho bạn trong đó bạn phải tạo ra một loạt các quy trình (với số được chỉ định vào thời gian chạy), kiểm soát chúng, phối hợp chúng và liên lạc giữa chúng. Tất nhiên không ai sẽ làm một cái gì đó tầm thường như thế trong cuộc sống thực. Nhưng, nếu bạn gặp một vấn đề lớn, có thể dễ dàng phân tách thành các phần có thể xử lý song song (ví dụ: phát hiện cạnh trong ảnh), việc cho phép bạn sử dụng đồng thời nhiều lõi CPU.
Scott

5

Có hai triết lý về quá trình tạo ra: ngã ba với sự kế thừa và tạo ra bằng các đối số. Unix sử dụng ngã ba, rõ ràng. (Ví dụ, OSE và VMS sử dụng phương thức tạo.) Unix có NHIỀU đặc điểm kế thừa và hơn thế nữa được thêm vào định kỳ. Thông qua thừa kế, những đặc điểm mới này có thể được thêm vào mà KHÔNG THAY ĐỔI CHƯƠNG TRÌNH HIỆN TẠI! Sử dụng mô hình tạo đối số, thêm các đặc điểm mới có nghĩa là thêm các đối số mới vào cuộc gọi tạo. Mô hình Unix đơn giản hơn.

Nó cũng liên kết mô hình fork-without-exec rất hữu ích, trong đó một quá trình có thể tự chia thành nhiều phần. Điều này rất quan trọng khi không có dạng I / O không đồng bộ và rất hữu ích khi tận dụng nhiều CPU trong một hệ thống. (Chủ đề trước.) Tôi đã làm điều này rất nhiều trong những năm qua, thậm chí gần đây. Về bản chất, nó cho phép chứa nhiều 'chương trình' trong một chương trình, do đó hoàn toàn không có chỗ cho tham nhũng hoặc phiên bản không khớp, v.v.

Mô hình fork / exec cũng cho phép một đứa trẻ cụ thể thừa hưởng một môi trường hoàn toàn kỳ lạ, được thiết lập giữa fork và exec. Những thứ như mô tả tập tin được kế thừa, đặc biệt. (Một phần mở rộng của stdio fd.) Mô hình tạo không cung cấp khả năng kế thừa bất cứ thứ gì không được hình dung bởi những người tạo ra cuộc gọi tạo.

Một số hệ thống cũng có thể hỗ trợ biên dịch động mã gốc, trong đó quá trình có hiệu lực bằng cách viết chương trình mã gốc của chính nó. Nói cách khác, nó muốn một chương trình mới mà nó đang tự viết, không cần phải trải qua chu trình mã nguồn / trình biên dịch / trình liên kết và chiếm không gian đĩa. (Tôi tin rằng có một hệ thống ngôn ngữ Verilog thực hiện việc này.) Mô hình fork hỗ trợ điều này, mô hình tạo thông thường sẽ không.


Các mô tả tập tin không phải là một phần mở rộng của stdioùi; con trỏ tập tin stdio là một trình bao bọc xung quanh mô tả tập tin. Các mô tả tệp xuất hiện đầu tiên và chúng là các thẻ điều khiển I / O Unix cơ bản. Nhưng, nếu không, đây là một điểm tốt.
Scott

2

Hàm fork () không chỉ sao chép tiến trình cha, nó trả về một giá trị tham chiếu rằng tiến trình đó là quá trình cha hoặc con trai, hình ảnh dưới đây giải thích cách bạn có thể sử dụng fork () như một người cha và một Con trai:

nhập mô tả hình ảnh ở đây

như được hiển thị khi tiến trình là cha fork () trả về ID tiến trình con trai, PID nó sẽ trả về0

ví dụ, bạn có thể sử dụng nó nếu bạn có một quy trình (máy chủ web) nhận được các yêu cầu và trên mỗi yêu cầu, nó tạo ra một son processquy trình để xử lý yêu cầu này, ở đây người cha và các con trai của họ có các công việc khác nhau.

Vì vậy, không chạy một bản sao của một quá trình không phải là điều chính xác như fork ().


5
Trong khi đó là sự thật, điều này không trả lời câu hỏi. Tại sao lại cần thiết cho quá trình tạo, nếu tôi muốn chạy một tệp thực thi khác?
SkyDan

1
Tôi đồng ý với SkyDan - điều này không trả lời câu hỏi. posix_spawn là một phiên bản hơi kỳ lạ của những gì có thể tưởng tượng 30 năm trước (trước khi Posix tồn tại) như là một hàm fork_execve ; một quá trình tạo ra một quy trình mới, khởi tạo hình ảnh của nó từ một tệp thực thi, thậm chí không gợi ý sao chép hình ảnh của tiến trình cha (ngoại trừ danh sách đối số, môi trường và các thuộc tính quy trình (ví dụ: thư mục làm việc)) và trả về PID của quy trình mới cho người gọi (quy trình cha) .
Scott

1
Có nhiều cách khác để truyền thông tin "cha mẹ" cho trẻ. Kỹ thuật giá trị trả về chỉ là cách hiệu quả nhất để thực hiện nó fork nếu bạn cho rằng bạn muốn forkở nơi đầu tiên
Cort Ammon

0

Chuyển hướng I / O được thực hiện dễ dàng nhất sau khi rẽ nhánh và trước khi thực hiện. Đứa trẻ, nhận thức được đó là đứa trẻ, có thể đóng mô tả tệp, mở cái mới, dup () hoặc dup2 () để đưa chúng vào đúng số fd, v.v., mà không ảnh hưởng đến cha mẹ. Sau khi làm điều đó, và có lẽ bất kỳ biến môi trường mong muốn nào thay đổi (cũng không ảnh hưởng đến cha mẹ), nó có thể thực thi chương trình mới trong môi trường phù hợp.


Tất cả những gì bạn đang làm ở đây là lặp lại đoạn thứ ba trong câu trả lời của Jim Cathey với một chi tiết nhỏ hơn một chút.
Scott

-2

Tôi nghĩ mọi người ở đây đều biết rằng fork hoạt động như thế nào, nhưng câu hỏi đặt ra là tại sao chúng ta cần tạo chính xác bản sao của cha mẹ bằng cách sử dụng fork? Trả lời ==> Lấy ví dụ về máy chủ (không có ngã ba), trong khi máy khách-1 đang truy cập máy chủ, nếu cùng lúc đó, máy khách thứ 2 đến và muốn truy cập máy chủ nhưng máy chủ không cấp phép cho máy chủ mới đến client-2 vì máy chủ đang bận phục vụ client-1 nên client-2 phải chờ. Sau khi tất cả các dịch vụ cho client-1 kết thúc, client-2 hiện có thể truy cập vào máy chủ. Bây giờ hãy xem xét nếu cùng một lúc client-3 đến, vì vậy client-3 phải đợi cho đến khi tất cả các dịch vụ cho client-2 kết thúc. Thực hiện kịch bản mà hàng ngàn khách hàng cần truy cập vào máy chủ cùng một lúc ... sau đó tất cả các máy khách phải chờ đã (máy chủ đang bận !!).

Điều này tránh được bằng cách tạo (sử dụng ngã ba) bản sao chính xác (tức là con) của máy chủ, trong đó mỗi đứa trẻ (là bản sao chính xác của máy chủ tức là máy chủ của nó) được dành riêng cho máy khách mới đến, do đó tất cả các máy khách đều truy cập như nhau người phục vụ.


Đây là lý do tại sao các quy trình máy chủ không nên được xử lý đơn luồng, xử lý các yêu cầu của máy khách liên tục khi chúng có thể được xử lý đồng thời - ví dụ: trong các quy trình riêng biệt. Nhưng mô hình máy chủ đa luồng có thể dễ dàng được thực hiện với quy trình người nghe chấp nhận các yêu cầu từ khách hàng và tạo ra một quy trình hoàn toàn mới để chạy chương trình dịch vụ khách. Ưu điểm duy nhất được cung cấp bởi forkcuộc gọi sao chép quy trình cha mẹ là bạn không phải có hai chương trình riêng biệt - nhưng có các chương trình riêng biệt (ví dụ inetd:) có thể làm cho hệ thống trở nên mô đun hơn.
Scott
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.