Lý do để thực hiện một ngã ba đôi khi tạo ra một daemon là gì?


165

Tôi đang cố gắng tạo ra một daemon trong python. Tôi đã tìm thấy câu hỏi sau đây , trong đó có một số tài nguyên tốt trong đó tôi hiện đang theo dõi, nhưng tôi tò mò về lý do tại sao một ngã ba đôi là cần thiết. Tôi đã tìm hiểu trên google và tìm thấy nhiều tài nguyên tuyên bố rằng một tài nguyên là cần thiết, nhưng không phải tại sao.

Một số đề cập rằng đó là để ngăn daemon có được một thiết bị đầu cuối kiểm soát. Làm thế nào nó sẽ làm điều này mà không có ngã ba thứ hai? Những hậu quả là gì?



2
Một khó khăn khi thực hiện một ngã ba đôi là cha mẹ không thể dễ dàng nhận được PID của quá trình cháu ( fork()cuộc gọi trả lại PID của con cho cha mẹ, vì vậy thật dễ dàng để có được PID của quy trình con, nhưng không dễ dàng như vậy có được PID của quá trình cháu ).
Craig McQueen

Câu trả lời:


105

Nhìn vào mã được tham chiếu trong câu hỏi, lời biện minh là:

Ngã ba một đứa trẻ thứ hai và thoát ra ngay lập tức để ngăn chặn zombie. Điều này làm cho quá trình con thứ hai bị mồ côi, làm cho quá trình init chịu trách nhiệm dọn dẹp nó. Và, vì đứa trẻ đầu tiên là một nhà lãnh đạo phiên không có thiết bị đầu cuối kiểm soát, nó có thể có được nó bằng cách mở một thiết bị đầu cuối trong tương lai (hệ thống dựa trên Hệ thống V). Cái ngã ba thứ hai này đảm bảo rằng đứa trẻ không còn là người dẫn đầu phiên, ngăn không cho daemon có được một thiết bị đầu cuối kiểm soát.

Vì vậy, đó là để đảm bảo rằng daemon được cấp lại cho init (chỉ trong trường hợp quá trình khởi động daemon tồn tại lâu) và loại bỏ bất kỳ cơ hội nào của daemon lấy lại tty kiểm soát. Vì vậy, nếu cả hai trường hợp này không áp dụng, thì một ngã ba là đủ. " Lập trình mạng Unix - Stevens " có một phần hay về vấn đề này.


28
Đây không phải là hoàn toàn chính xác. Cách tiêu chuẩn để tạo một daemon là chỉ cần làm p=fork(); if(p) exit(); setsid(). Trong trường hợp này, cha mẹ cũng thoát ra và quá trình con đầu tiên được sửa chữa. Phép thuật hai ngã ba chỉ được yêu cầu để ngăn daemon có được một tty.
parasietje

1
Vì vậy, theo tôi hiểu, nếu chương trình của tôi bắt đầu và forksmột childquy trình, quy trình con đầu tiên này sẽ là một session leadervà sẽ có thể mở một thiết bị đầu cuối TTY. Nhưng nếu tôi rẽ nhánh một lần nữa từ đứa trẻ này và chấm dứt đứa con đầu tiên này, đứa trẻ ngã ba thứ hai sẽ không thể session leadervà sẽ không thể mở một thiết bị đầu cuối TTY. Là tuyên bố này đúng?
tonix

2
@tonix: chỉ đơn giản là không tạo ra một nhà lãnh đạo phiên. Điều đó được thực hiện bởi setsid(). Vì vậy, quy trình rẽ nhánh đầu tiên trở thành người dẫn đầu phiên sau khi gọi setsid()và sau đó chúng ta rẽ nhánh một lần nữa để quá trình rẽ đôi cuối cùng không còn là người dẫn đầu phiên. Khác với yêu cầu setsid()trở thành người lãnh đạo phiên, bạn được chú ý.
dbmikus

169

Tôi đã cố gắng để hiểu về ngã ba đôi và vấp phải câu hỏi này ở đây. Sau rất nhiều nghiên cứu, đây là những gì tôi tìm ra. Hy vọng nó sẽ giúp làm rõ mọi thứ tốt hơn cho bất cứ ai có cùng câu hỏi.

Trong Unix, mọi tiến trình thuộc về một nhóm, lần lượt thuộc về một phiên. Đây là hệ thống phân cấp

Phiên (SID) → Nhóm quy trình (PGID) → Quy trình (PID)

Quá trình đầu tiên trong nhóm quy trình trở thành người lãnh đạo nhóm quy trình và quy trình đầu tiên trong phiên trở thành người lãnh đạo phiên. Mỗi phiên có thể có một TTY liên kết với nó. Chỉ người lãnh đạo phiên có thể kiểm soát TTY. Để một quá trình thực sự được trình bày (chạy trong nền), chúng ta nên đảm bảo rằng người lãnh đạo phiên bị giết để không có khả năng phiên nào chiếm quyền kiểm soát TTY.

Tôi đã chạy chương trình daemon ví dụ python của Sander Marechal từ trang web này trên Ubuntu của tôi. Dưới đây là kết quả với ý kiến ​​của tôi.

1. `Parent`    = PID: 28084, PGID: 28084, SID: 28046
2. `Fork#1`    = PID: 28085, PGID: 28084, SID: 28046
3. `Decouple#1`= PID: 28085, PGID: 28085, SID: 28085
4. `Fork#2`    = PID: 28086, PGID: 28085, SID: 28085

Lưu ý rằng quy trình là người dẫn đầu phiên sau Decouple#1, bởi vì đó là PID = SID. Nó vẫn có thể kiểm soát một TTY.

Lưu ý rằng Fork#2không còn là người lãnh đạo phiên PID != SID. Quá trình này không bao giờ có thể kiểm soát một TTY. Quả thật daemon.

Cá nhân tôi thấy thuật ngữ ngã ba lần là khó hiểu. Một thành ngữ tốt hơn có thể là fork-decouple-fork.

Liên kết quan tâm bổ sung:


Ngã ba hai lần cũng ngăn tạo zombie khi tiến trình cha chạy trong thời gian dài hơn và vì một số lý do loại bỏ trình xử lý mặc định để báo hiệu cho biết quá trình đó đã chết.
Trismegistos

Nhưng thứ hai cũng có thể gọi decouple và trở thành trưởng nhóm và sau đó có được thiết bị đầu cuối.
Trismegistos

2
Đây không phải là sự thật. Việc đầu tiên fork()đã ngăn chặn việc tạo ra zombie, miễn là bạn đóng cha mẹ.
parasietje

1
Một ví dụ tối thiểu để tạo ra các kết quả được trích dẫn ở trên: gist.github.com/cannium/7aa58f13c834920bb32c
can.

1
Nó sẽ là tốt để gọi setsid() trước một đơn fork()? Thật ra tôi đoán câu trả lời của câu hỏi này trả lời rằng.
Craig McQueen

118

Nói một cách chính xác, nĩa đôi không liên quan gì đến việc nuôi dạy lại con daemon khi còn nhỏ init. Tất cả những gì cần thiết để nuôi dạy lại đứa trẻ là cha mẹ phải thoát ra. Điều này có thể được thực hiện chỉ với một ngã ba duy nhất. Ngoài ra, thực hiện một ngã ba đôi tự nó không tái định nghĩa quá trình trình nền init; cha của daemon phải thoát ra. Nói cách khác, cha mẹ luôn thoát ra khi tạo một daemon thích hợp để quá trình daemon được phát hiện lại init.

Vậy tại sao ngã ba đôi? POSIX.1-2008 Mục 11.1.3, " Thiết bị đầu cuối kiểm soát ", có câu trả lời (nhấn mạnh thêm):

Thiết bị đầu cuối kiểm soát cho một phiên được phân bổ bởi người lãnh đạo phiên theo cách xác định thực hiện. Nếu một nhà lãnh đạo phiên không có thiết bị đầu cuối kiểm soát và mở tệp thiết bị đầu cuối chưa được liên kết với phiên mà không sử dụng O_NOCTTYtùy chọn (xem open()), thì nó được xác định theo thực thi liệu thiết bị đầu cuối có trở thành thiết bị đầu cuối kiểm soát của nhà lãnh đạo phiên hay không. Nếu một quá trình đó là không một nhà lãnh đạo phiên mở một tập tin thiết bị đầu cuối, hoặc các O_NOCTTYtùy chọn được sử dụng trên open(), sau đó thiết bị đầu cuối sẽ không trở thành thiết bị đầu cuối kiểm soát của quá trình gọi .

Điều này cho chúng ta biết rằng nếu một quy trình daemon làm điều gì đó như thế này ...

int fd = open("/dev/console", O_RDWR);

... Sau đó, quy trình trình nền có thể có được /dev/consolenhư là thiết bị đầu cuối kiểm soát của nó, tùy thuộc vào việc quy trình trình nền có phải là người dẫn đầu phiên hay không và tùy thuộc vào việc triển khai hệ thống. Chương trình có thể đảm bảo rằng cuộc gọi trên sẽ không có được thiết bị đầu cuối kiểm soát nếu trước tiên chương trình đảm bảo rằng đó không phải là người dẫn đầu phiên.

Thông thường, khi khởi chạy một daemon, setsidđược gọi (từ tiến trình con sau khi gọi fork) để phân tách daemon khỏi thiết bị đầu cuối kiểm soát của nó. Tuy nhiên, gọi setsidcũng có nghĩa là quá trình gọi sẽ là người lãnh đạo phiên của phiên mới, điều này mở ra khả năng daemon có thể yêu cầu thiết bị đầu cuối kiểm soát. Kỹ thuật fork đôi đảm bảo rằng quy trình daemon không phải là người dẫn đầu phiên, sau đó đảm bảo rằng một cuộc gọi đến open, như trong ví dụ trên, sẽ không dẫn đến quá trình daemon lấy lại thiết bị đầu cuối điều khiển.

Kỹ thuật double-fork là một chút hoang tưởng. Có thể không cần thiết nếu bạn biết rằng trình nền sẽ không bao giờ mở tệp thiết bị đầu cuối. Ngoài ra, trên một số hệ thống, có thể không cần thiết ngay cả khi daemon mở tệp thiết bị đầu cuối, vì hành vi đó được xác định theo thực thi. Tuy nhiên, một điều không được xác định theo triển khai là chỉ có người lãnh đạo phiên có thể phân bổ thiết bị đầu cuối kiểm soát. Nếu một quá trình không phải là một nhà lãnh đạo phiên, nó không thể phân bổ một thiết bị đầu cuối kiểm soát. Do đó, nếu bạn muốn bị hoang tưởng và chắc chắn rằng quy trình daemon không thể vô tình có được một thiết bị đầu cuối kiểm soát, bất kể mọi chi tiết cụ thể do triển khai thực hiện, thì kỹ thuật hai ngã ba là rất cần thiết.


3
+1 Quá tệ câu trả lời này đã đến ~ bốn năm sau khi câu hỏi được hỏi.
Tim Seguine

12
Nhưng điều đó vẫn không giải thích được tại sao nó cực kỳ quan trọng đến nỗi một daemon không thể truy vấn thiết bị đầu cuối kiểm soát
UloPe

7
Từ khóa "vô tình" có được một thiết bị đầu cuối kiểm soát. Nếu quá trình xảy ra để mở một thiết bị đầu cuối và nó trở thành thiết bị kiểm soát các thiết bị đầu cuối, hơn là nếu ai đó phát hành ^ C từ thiết bị đầu cuối đó, nó có thể chấm dứt quá trình. Vì vậy, thật tốt khi bảo vệ một quá trình khỏi việc vô tình xảy ra với nó. Cá nhân tôi sẽ dính vào một ngã ba và setsid () cho mã tôi viết mà tôi biết sẽ không mở các thiết bị đầu cuối.
BobDoolittle

1
@BobDoolittle làm thế nào điều này có thể xảy ra "vô tình"? Một quá trình sẽ không chỉ dừng lại ở việc mở các thiết bị đầu cuối nếu nó không được viết để làm như vậy. Có lẽ forking đôi là hữu ích nếu bạn lập trình viên không biết mã và không biết nếu nó có thể mở một tty.
Marius

10
@Marius Hãy tưởng tượng những gì có thể xảy ra nếu bạn thêm một dòng như thế này vào tệp cấu hình của daemon của bạn : LogFile=/dev/console. Các chương trình không phải lúc nào cũng có quyền kiểm soát thời gian biên dịch đối với những tệp nào chúng có thể mở;)
Dan Moulding

11

Lấy từ CTK xấu :

"Trên một số hương vị của Unix, bạn buộc phải thực hiện một cú đúp khi khởi động, để chuyển sang chế độ daemon. Điều này là do việc tách đơn không được đảm bảo tách khỏi thiết bị đầu cuối điều khiển."


3
Làm thế nào để một ngã ba không tách ra khỏi thiết bị đầu cuối kiểm soát nhưng ngã ba đôi làm như vậy? Điều gì unixes này xảy ra trên?
bdonlan

12
Một daemon phải đóng bộ mô tả tệp đầu vào và đầu ra (fds), nếu không, nó sẽ vẫn được gắn vào thiết bị đầu cuối mà nó đã được khởi động. Một quá trình rẽ nhánh kế thừa những cái từ cha mẹ. Rõ ràng, đứa trẻ đầu tiên đóng fds nhưng điều đó không làm sạch mọi thứ. Trên ngã ba thứ hai, fds không tồn tại, vì vậy đứa trẻ thứ hai không thể kết nối với bất cứ điều gì nữa.
Aaron Digulla

4
@Aaron: Không, một daemon đúng cách "tách" khỏi thiết bị đầu cuối kiểm soát của nó bằng cách gọi setsidsau một ngã ba ban đầu. Sau đó nó đảm bảo rằng nó vẫn được tách ra từ một thiết bị đầu cuối kiểm soát bởi forking một lần nữa và có lãnh đạo phiên (quá trình đó gọi là setsid) thoát.
Dan Mould

2
@bdonlan: Nó không forktách rời khỏi thiết bị đầu cuối kiểm soát. Đó là setsidnó làm điều đó. Nhưng setsidsẽ thất bại nếu nó được gọi từ một nhóm trưởng quy trình. Vì vậy, một khởi đầu forkphải được thực hiện trước setsidđể đảm bảo setsidđược gọi từ một quy trình không phải là người lãnh đạo nhóm quy trình. Thứ hai forkđảm bảo rằng quy trình cuối cùng (quy trình sẽ là daemon) không phải là người lãnh đạo phiên. Chỉ những người lãnh đạo phiên mới có thể có được thiết bị đầu cuối kiểm soát, vì vậy ngã ba thứ hai này đảm bảo rằng daemon sẽ không vô tình yêu cầu thiết bị đầu cuối kiểm soát. Điều này đúng với bất kỳ HĐH POSIX nào.
Dan Mould

@DanMoulding Điều này không đảm bảo rằng đứa trẻ thứ hai sẽ không có được thiết bị đầu cuối kiểm soát bởi vì nó có thể gọi setsid và trở thành người lãnh đạo phiên và sau đó có được thiết bị đầu cuối kiểm soát.
Trismegistos

7

Theo "Lập trình nâng cao trong môi trường Unix", bởi Stephens và Rago, ngã ba thứ hai là một khuyến nghị và nó được thực hiện để đảm bảo rằng daemon không có được thiết bị đầu cuối kiểm soát trên các hệ thống dựa trên System V.


3

Một lý do là quá trình cha mẹ có thể ngay lập tức chờ đợi_pid () cho đứa trẻ và sau đó quên nó đi. Khi đó, cháu nội chết, cha mẹ là init và nó sẽ đợi () cho nó - và đưa nó ra khỏi trạng thái zombie.

Kết quả là quá trình cha mẹ không cần phải biết về những đứa trẻ rẽ nhánh, và nó cũng làm cho nó có thể rẽ nhánh các quá trình chạy dài từ libs, v.v.


2

Cuộc gọi daemon () có lệnh gọi cha _exit () nếu thành công. Động lực ban đầu có thể là cho phép cha mẹ làm một số công việc phụ trong khi đứa trẻ đang daemon.

Nó cũng có thể dựa trên một niềm tin sai lầm rằng nó là cần thiết để đảm bảo daemon không có quá trình cha mẹ và được sửa chữa lại cho init - nhưng điều này sẽ xảy ra dù sao đi nữa khi cha mẹ chết trong trường hợp ngã ba.

Vì vậy, tôi cho rằng cuối cùng tất cả chỉ sôi sục theo truyền thống - một ngã ba duy nhất là đủ miễn là cha mẹ chết theo thứ tự ngắn nào.


2

Một cuộc thảo luận tốt về nó dường như có tại http://www.developerweb.net/forum/showthread.php?t=3025

Trích dẫn mlampkin từ đó:

... nghĩ về cuộc gọi setsid () như là cách "mới" để thực hiện (tách rời khỏi thiết bị đầu cuối) và cuộc gọi [second] fork () sau khi nó là dự phòng để đối phó với SVr4 ...


-1

Có thể dễ hiểu hơn theo cách này:

  • Nĩa đầu tiên và setsid sẽ tạo một phiên mới (nhưng ID tiến trình == ID phiên).
  • Ngã ba thứ hai đảm bảo ID tiến trình! = ID phiên.
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.