Có cách nào để nhiều tiến trình chia sẻ một ổ cắm lắng nghe không?


90

Trong lập trình ổ cắm, bạn tạo một ổ cắm lắng nghe và sau đó đối với mỗi máy khách kết nối, bạn sẽ có một ổ cắm luồng bình thường mà bạn có thể sử dụng để xử lý yêu cầu của khách hàng. Hệ điều hành quản lý hàng đợi các kết nối đến đằng sau hậu trường.

Hai tiến trình không thể liên kết với cùng một cổng cùng một lúc - theo mặc định.

Tôi tự hỏi liệu có cách nào (trên bất kỳ hệ điều hành nổi tiếng nào, đặc biệt là Windows) để khởi chạy nhiều phiên bản của một quy trình, sao cho tất cả chúng đều liên kết với socket và do đó chúng chia sẻ hàng đợi một cách hiệu quả. Mỗi cá thể quy trình sau đó có thể là một luồng đơn; nó sẽ chỉ chặn khi chấp nhận một kết nối mới. Khi một máy khách được kết nối, một trong các phiên bản quy trình không hoạt động sẽ chấp nhận máy khách đó.

Điều này sẽ cho phép mỗi quy trình có một triển khai đơn luồng rất đơn giản, không chia sẻ gì trừ khi thông qua bộ nhớ được chia sẻ rõ ràng và người dùng có thể điều chỉnh băng thông xử lý bằng cách bắt đầu nhiều phiên bản hơn.

Tính năng như vậy có tồn tại không?

Chỉnh sửa: Đối với những người hỏi "Tại sao không sử dụng chủ đề?" Rõ ràng chủ đề là một lựa chọn. Nhưng với nhiều luồng trong một quy trình, tất cả các đối tượng đều có thể chia sẻ được và cần phải hết sức cẩn thận để đảm bảo rằng các đối tượng hoặc không được chia sẻ hoặc chỉ hiển thị với một luồng tại một thời điểm hoặc hoàn toàn bất biến và các ngôn ngữ phổ biến nhất và thời gian chạy thiếu hỗ trợ tích hợp để quản lý sự phức tạp này.

Bằng cách bắt đầu một số ít các quy trình worker giống nhau, bạn sẽ nhận được một hệ thống đồng thời, trong đó mặc định là không chia sẻ, giúp dễ dàng hơn nhiều trong việc xây dựng triển khai đúng và có thể mở rộng.


2
Tôi đồng ý, nhiều quy trình có thể giúp dễ dàng hơn trong việc tạo ra một triển khai chính xác và mạnh mẽ. Có thể mở rộng, tôi không chắc, nó phụ thuộc vào miền sự cố của bạn.
MarkR

Câu trả lời:


92

Bạn có thể chia sẻ một socket giữa hai (hoặc nhiều) quy trình trong Linux và thậm chí cả Windows.

Trong hệ điều hành Linux (Hoặc hệ điều hành loại POSIX), việc sử dụng fork()sẽ khiến phần tử con được chia nhỏ có bản sao của tất cả các bộ mô tả tệp của cha mẹ. Mọi thứ mà nó không đóng sẽ tiếp tục được chia sẻ và (ví dụ với ổ cắm lắng nghe TCP) có thể được sử dụng cho accept()các ổ cắm mới cho máy khách. Đây là cách nhiều máy chủ, bao gồm cả Apache trong hầu hết các trường hợp, hoạt động.

Trên Windows, điều tương tự về cơ bản là đúng, ngoại trừ không có fork()lệnh gọi hệ thống nên tiến trình mẹ sẽ cần sử dụng CreateProcesshoặc một cái gì đó để tạo một quy trình con (tất nhiên có thể sử dụng cùng một tệp thực thi) và cần chuyển cho nó một xử lý kế thừa.

Làm cho một ổ cắm lắng nghe trở thành một tay cầm có thể kế thừa không phải là một hoạt động hoàn toàn tầm thường nhưng cũng không quá phức tạp. DuplicateHandle()cần được sử dụng để tạo một xử lý trùng lặp (tuy nhiên vẫn trong quy trình mẹ), xử lý này sẽ có cờ kế thừa được đặt trên đó. Sau đó, bạn có thể cung cấp xử lý đó trong STARTUPINFOcấu trúc cho tiến trình con trong CreateProcess dưới dạng STDIN, OUThoặc ERRxử lý (giả sử bạn không muốn sử dụng nó cho bất kỳ điều gì khác).

BIÊN TẬP:

Đọc thư viện MDSN, có vẻ như đó WSADuplicateSocketlà một cơ chế mạnh mẽ hơn hoặc chính xác hơn để thực hiện việc này; nó vẫn không quan trọng vì các quy trình cha / con cần phải tìm ra xử lý nào cần được nhân bản bởi một số cơ chế IPC (mặc dù điều này có thể đơn giản như một tệp trong hệ thống tệp)

LÀM RÕ:

Trả lời cho câu hỏi ban đầu của OP, không, nhiều quy trình không thể bind(); chỉ là quá trình cha mẹ ban đầu sẽ gọi bind(), listen()vv, các quá trình con sẽ chỉ xử lý yêu cầu của accept(), send(), recv(), vv


3
Nhiều quy trình có thể liên kết bằng cách chỉ định tùy chọn socket SocketOptionName.ReuseAddress.
nhâm nhi

Nhưng vấn đề là gì? Dù sao thì quy trình cũng nặng hơn luồng.
Anton Tykhyy

7
Các quy trình nặng hơn các luồng, nhưng vì chúng chỉ chia sẻ những thứ được chia sẻ một cách rõ ràng, yêu cầu ít đồng bộ hóa hơn, giúp lập trình dễ dàng hơn và thậm chí có thể hiệu quả hơn trong một số trường hợp.
MarkR

11
Hơn nữa, nếu một quy trình con gặp sự cố hoặc bị hỏng theo một cách nào đó, nó sẽ ít có khả năng ảnh hưởng đến cha mẹ hơn.
MarkR

3
Cũng cần lưu ý rằng, trong linux, bạn có thể "truyền" các socket cho các chương trình khác mà không cần sử dụng fork () và không có mối quan hệ cha / con, bằng cách sử dụng Unix Sockets.
Rahly

34

Hầu hết những người khác đã cung cấp các lý do kỹ thuật tại sao điều này hoạt động. Đây là một số mã python bạn có thể chạy để chứng minh điều này cho chính mình:

import socket
import os

def main():
    serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serversocket.bind(("127.0.0.1", 8888))
    serversocket.listen(0)

    # Child Process
    if os.fork() == 0:
        accept_conn("child", serversocket)

    accept_conn("parent", serversocket)

def accept_conn(message, s):
    while True:
        c, addr = s.accept()
        print 'Got connection from in %s' % message
        c.send('Thank you for your connecting to %s\n' % message)
        c.close()

if __name__ == "__main__":
    main()

Lưu ý rằng thực sự có hai quá trình id đang lắng nghe:

$ lsof -i :8888
COMMAND   PID    USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
Python  26972 avaitla    3u  IPv4 0xc26aa26de5a8fc6f      0t0  TCP localhost:ddi-tcp-1 (LISTEN)
Python  26973 avaitla    3u  IPv4 0xc26aa26de5a8fc6f      0t0  TCP localhost:ddi-tcp-1 (LISTEN)

Đây là kết quả từ việc chạy telnet và chương trình:

$ telnet 127.0.0.1 8888
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Thank you for your connecting to parent
Connection closed by foreign host.
$ telnet 127.0.0.1 8888
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Thank you for your connecting to child
Connection closed by foreign host.
$ telnet 127.0.0.1 8888
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Thank you for your connecting to parent
Connection closed by foreign host.

$ python prefork.py 
Got connection from in parent
Got connection from in child
Got connection from in parent

2
Vì vậy, đối với một kết nối, cha mẹ hoặc con cái đều nhận được nó. Nhưng ai nhận được kết nối là không xác định, phải không?
Hot.PxL

1
vâng, tôi nghĩ rằng nó phụ thuộc vào quá trình được lập lịch để chạy bởi hệ điều hành.
Anil Vaitla

14

Tôi muốn nói thêm rằng các socket có thể được chia sẻ trên Unix / Linux qua các socket AF__UNIX (các socket liên quá trình). Điều dường như sẽ xảy ra là một bộ mô tả ổ cắm mới được tạo ra có phần giống với bí danh của bộ mô tả gốc. Bộ mô tả ổ cắm mới này được gửi qua ổ cắm AFUNIX tới quy trình khác. Điều này đặc biệt hữu ích trong trường hợp một tiến trình không thể fork () để chia sẻ các bộ mô tả tệp của nó. Ví dụ: khi sử dụng các thư viện ngăn chặn điều này do các vấn đề phân luồng. Bạn nên tạo một ổ cắm miền Unix và sử dụng libancillary để gửi qua bộ mô tả.

Xem:

Để tạo AF_UNIX Sockets:

Ví dụ mã:


13

Có vẻ như câu hỏi này đã được trả lời đầy đủ bởi MarkR và zackthehack nhưng tôi muốn nói thêm rằng Nginx là một ví dụ về mô hình kế thừa socket lắng nghe.

Đây là một mô tả tốt:

         Implementation of HTTP Auth Server Round-Robin and
                Memory Caching for NGINX Email Proxy

                            June 6, 2007
             Md. Mansoor Peerbhoy <mansoor@zimbra.com>

...

Luồng quy trình NGINX worker

Sau khi quy trình NGINX chính đọc tệp cấu hình và phân nhánh vào số quy trình công nhân đã định cấu hình, mỗi quy trình công nhân sẽ đi vào một vòng lặp nơi nó chờ bất kỳ sự kiện nào trên tập hợp các ổ cắm tương ứng của nó.

Mỗi quy trình công nhân bắt đầu chỉ với các ổ cắm lắng nghe, vì chưa có kết nối nào. Do đó, bộ mô tả sự kiện được đặt cho mỗi quy trình công nhân bắt đầu chỉ với các ổ cắm lắng nghe.

(LƯU Ý) NGINX có thể được định cấu hình để sử dụng bất kỳ một trong số các cơ chế bỏ phiếu sự kiện: aio / devpoll / epoll / eventpoll / kqueue / thăm dò / rtsig / select

Khi một kết nối đến trên bất kỳ ổ cắm lắng nghe nào (POP3 / IMAP / SMTP), mỗi quy trình công nhân sẽ xuất hiện từ cuộc thăm dò sự kiện của nó, vì mỗi quy trình công nhân NGINX kế thừa ổ cắm lắng nghe. Sau đó, mỗi quy trình NGINX worker sẽ cố gắng lấy mutex toàn cầu. Một trong các quy trình công nhân sẽ có được khóa, trong khi các quy trình khác sẽ quay lại vòng bỏ phiếu sự kiện tương ứng của chúng.

Trong khi đó, quy trình công nhân đã có được mutex toàn cầu sẽ kiểm tra các sự kiện được kích hoạt và sẽ tạo ra các yêu cầu hàng đợi công việc cần thiết cho mỗi sự kiện đã được kích hoạt. Một sự kiện tương ứng với một bộ mô tả socket duy nhất từ ​​tập hợp các bộ mô tả mà nhân viên đang theo dõi các sự kiện từ đó.

Nếu sự kiện được kích hoạt tương ứng với một kết nối mới đến, NGINX chấp nhận kết nối từ ổ cắm lắng nghe. Sau đó, nó liên kết cấu trúc dữ liệu ngữ cảnh với trình mô tả tệp. Ngữ cảnh này chứa thông tin về kết nối (liệu POP3 / IMAP / SMTP, người dùng đã được xác thực chưa, v.v.). Sau đó, ổ cắm mới được xây dựng này được thêm vào bộ mô tả sự kiện cho quy trình công nhân đó.

Công nhân hiện từ bỏ mutex (có nghĩa là bất kỳ sự kiện nào xảy ra trên các nhân viên khác đều có thể xảy ra) và bắt đầu xử lý từng yêu cầu đã được xếp hàng trước đó. Mỗi yêu cầu tương ứng với một sự kiện đã được báo hiệu. Từ mỗi bộ mô tả socket đã được báo hiệu, quá trình worker truy xuất cấu trúc dữ liệu ngữ cảnh tương ứng được liên kết trước đó với bộ mô tả đó, sau đó gọi các hàm gọi lại tương ứng thực hiện các hành động dựa trên trạng thái của kết nối đó. Ví dụ: trong trường hợp kết nối IMAP mới được thiết lập, điều đầu tiên mà NGINX sẽ làm là viết thông báo chào mừng IMAP tiêu chuẩn vào
ổ cắm được kết nối (* OK IMAP4 đã sẵn sàng).

Dần dần, mỗi quy trình công nhân sẽ hoàn thành việc xử lý mục nhập hàng đợi công việc cho mỗi sự kiện nổi bật và quay trở lại vòng lặp bỏ phiếu sự kiện của nó. Khi bất kỳ kết nối nào được thiết lập với máy khách, các sự kiện thường nhanh hơn, vì bất cứ khi nào ổ cắm được kết nối sẵn sàng để đọc, thì sự kiện đọc được kích hoạt và hành động tương ứng phải được thực hiện.


11

Không chắc điều này có liên quan như thế nào với câu hỏi ban đầu, nhưng trong nhân Linux 3.9 có một bản vá bổ sung tính năng TCP / UDP: hỗ trợ TCP và UDP cho tùy chọn socket SO_REUSEPORT; Tùy chọn socket mới cho phép nhiều socket trên cùng một máy chủ liên kết với cùng một cổng và nhằm cải thiện hiệu suất của các ứng dụng máy chủ mạng đa luồng chạy trên các hệ thống đa lõi. có thể tìm thấy thêm thông tin trong liên kết LWN SO_REUSEPORT trong Linux Kernel 3.9 như được đề cập trong liên kết tham khảo:

tùy chọn SO_REUSEPORT không phải là tiêu chuẩn, nhưng có sẵn ở dạng tương tự trên một số hệ thống UNIX khác (đáng chú ý là BSDs, nơi bắt nguồn ý tưởng). Nó dường như cung cấp một giải pháp thay thế hữu ích để giảm hiệu suất tối đa của các ứng dụng mạng chạy trên các hệ thống đa lõi mà không cần phải sử dụng mô hình fork.


Từ bài viết LWN, nó gần giống như SO_REUSEPORTtạo một nhóm luồng, trong đó mỗi ổ cắm nằm trên một luồng khác nhau nhưng chỉ một ổ cắm trong nhóm thực hiện accept. Bạn có thể xác nhận tất cả các ổ cắm trong nhóm đều nhận được một bản sao dữ liệu không?
jww


3

Có một nhiệm vụ duy nhất có công việc duy nhất là lắng nghe các kết nối đến. Khi một kết nối được nhận, nó sẽ chấp nhận kết nối - điều này tạo ra một bộ mô tả ổ cắm riêng biệt. Ổ cắm được chấp nhận được chuyển cho một trong các tác vụ công nhân có sẵn của bạn và tác vụ chính quay lại lắng nghe.

s = socket();
bind(s);
listen(s);
while (1) {
  s2 = accept(s);
  send_to_worker(s2);
}

Làm thế nào ổ cắm được chuyển cho một công nhân? Hãy nhớ rằng ý tưởng rằng một công nhân là một quá trình riêng biệt.
Daniel Earwicker

fork () có lẽ, hoặc một trong những ý tưởng khác ở trên. Hoặc có thể bạn tách hoàn toàn I / O socket khỏi xử lý dữ liệu; gửi tải trọng đến các quy trình của worker thông qua cơ chế IPC. OpenSSH và các công cụ OpenBSD khác sử dụng phương pháp này (không có luồng).
HUAGHAGUAH

3

Trong Windows (và Linux), một quy trình có thể mở một ổ cắm và sau đó chuyển ổ cắm đó sang một quy trình khác để quy trình thứ hai sau đó cũng có thể sử dụng ổ cắm đó (và chuyển nó lần lượt, nếu nó muốn làm như vậy) .

Lệnh gọi hàm quan trọng là WSADuplicateSocket ().

Điều này điền vào một cấu trúc với thông tin về một ổ cắm hiện có. Sau đó, cấu trúc này, thông qua một cơ chế IPC mà bạn chọn, được chuyển đến một quy trình hiện có khác (lưu ý rằng tôi nói là hiện có - khi bạn gọi WSADuplicateSocket (), bạn phải chỉ ra quy trình đích sẽ nhận thông tin được phát ra).

Sau đó, quá trình nhận có thể gọi WSASocket (), truyền cấu trúc thông tin này và nhận một xử lý đến ổ cắm bên dưới.

Cả hai quy trình hiện giữ một chốt cho cùng một ổ cắm bên dưới.


2

Có vẻ như những gì bạn muốn là một quá trình lắng nghe các khách hàng mới và sau đó ngắt kết nối khi bạn có kết nối. Để làm điều đó trên các luồng thật dễ dàng và trong .Net bạn thậm chí có các phương thức BeginAccept, v.v. để chăm sóc rất nhiều hệ thống ống nước cho bạn. Để loại bỏ các kết nối qua các ranh giới quy trình sẽ phức tạp và không có bất kỳ lợi thế nào về hiệu suất.

Ngoài ra, bạn có thể có nhiều tiến trình bị ràng buộc và lắng nghe trên cùng một ổ cắm.

TcpListener tcpServer = new TcpListener(IPAddress.Loopback, 10090);
tcpServer.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
tcpServer.Start();

while (true)
{
    TcpClient client = tcpServer.AcceptTcpClient();
    Console.WriteLine("TCP client accepted from " + client.Client.RemoteEndPoint + ".");
}

Nếu bạn kích hoạt hai quá trình, mỗi quá trình thực thi đoạn mã trên, nó sẽ hoạt động và quá trình đầu tiên dường như nhận được tất cả các kết nối. Nếu quá trình đầu tiên bị giết, quá trình thứ hai sẽ nhận được các kết nối. Với việc chia sẻ socket như vậy, tôi không chắc chắn chính xác cách Windows quyết định quy trình nào nhận được kết nối mới mặc dù kiểm tra nhanh chỉ ra quy trình cũ nhất nhận được chúng trước. Về việc liệu nó có chia sẻ nếu quy trình đầu tiên đang bận hay bất cứ điều gì tương tự như vậy không thì tôi không biết.


2

Một cách tiếp cận khác (tránh nhiều chi tiết phức tạp) trong Windows nếu bạn đang sử dụng HTTP, là sử dụng HTTP.SYS . Điều này cho phép nhiều quy trình lắng nghe các URL khác nhau trên cùng một cổng. Trên Server 2003/2008 / Vista / 7, đây là cách IIS hoạt động, vì vậy bạn có thể chia sẻ các cổng với nó. (Trên XP SP2 HTTP.SYS được hỗ trợ, nhưng IIS5.1 không sử dụng nó.)

Các API cấp cao khác (bao gồm WCF) sử dụng HTTP.SYS.

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.