2012-10-04 20 views
21

Tôi có máy chủ đa luồng (nhóm luồng) đang xử lý một số lượng lớn yêu cầu (tối đa 500/giây cho một nút), sử dụng 20 chuỗi. Có một chuỗi trình nghe chấp nhận các kết nối đến và xếp hàng chúng để xử lý các luồng xử lý. Khi đáp ứng đã sẵn sàng, các luồng sau đó ghi ra máy khách và đóng socket. Tất cả dường như tốt cho đến gần đây, một chương trình thử nghiệm của khách hàng bắt đầu treo ngẫu nhiên sau khi đọc câu trả lời. Sau nhiều lần đào, có vẻ như phần đóng() từ máy chủ không thực sự ngắt kết nối ổ cắm. Tôi đã thêm một số bản in gỡ lỗi vào mã với số mô tả tập tin và tôi nhận được kiểu đầu ra này.close() không đóng đúng ổ cắm

Processing request for 21 
Writing to 21 
Closing 21 

Giá trị trả về của gần() bằng 0, hoặc sẽ có một câu lệnh gỡ lỗi khác được in. Sau khi đầu ra này với một khách hàng bị treo, lsof đang hiển thị một kết nối đã được thiết lập.

MÁY CHỦ 8160 gốc 21U IPv4 32.754.237 TCP localhost: 9980-> localhost: 47.530 (ESTABLISHED)

KHÁCH HÀNG 17.747 gốc 12u IPv4 32.754.228 TCP localhost: 47530-> localhost: 9980 (ESTABLISHED)

Cứ như nếu máy chủ không bao giờ gửi trình tự tắt cho máy khách và trạng thái này bị treo cho đến khi máy khách bị giết, hãy để máy chủ ở trạng thái chờ gần như

MÁY CHỦ 8160 root 21u IPv4 32754237 TCP localhost: 9980-> localhost: 47530 (CLOSE_WAIT)

Ngoài ra nếu khách hàng có thời gian chờ được chỉ định, nó sẽ hết thời gian chờ thay vì treo. Tôi cũng có thể chạy theo cách thủ công

call close(21) 

trong máy chủ từ gdb, sau đó khách hàng sẽ ngắt kết nối. Điều này xảy ra có thể một lần trong 50.000 yêu cầu, nhưng có thể không xảy ra trong thời gian dài.

Linux phiên bản: 2.6.21.7-2.fc8xen Centos phiên bản: 5.4 (Final)

hành động ổ cắm như sau

SERVER:

int client_socket; struct sockaddr_in client_addr; socklen_t client_len = sizeof (client_addr);

while(true) { 
    client_socket = accept(incoming_socket, (struct sockaddr *)&client_addr, &client_len); 
    if (client_socket == -1) 
    continue; 
    /* insert into queue here for threads to process */ 
} 

Sau đó, chủ đề chọn ổ cắm và tạo phản hồi.

/* get client_socket from queue */ 

/* processing request here */ 

/* now set to blocking for write; was previously set to non-blocking for reading */ 
int flags = fcntl(client_socket, F_GETFL); 
if (flags < 0) 
    abort(); 
if (fcntl(client_socket, F_SETFL, flags|O_NONBLOCK) < 0) 
    abort(); 

server_write(client_socket, response_buf, response_length); 
server_close(client_socket); 

server_write và server_close.

void server_write(int fd, char const *buf, ssize_t len) { 
    printf("Writing to %d\n", fd); 
    while(len > 0) { 
     ssize_t n = write(fd, buf, len); 
     if(n <= 0) 
     return;// I don't really care what error happened, we'll just drop the connection 
     len -= n; 
     buf += n; 
    } 
    } 

void server_close(int fd) { 
    for(uint32_t i=0; i<10; i++) { 
     int n = close(fd); 
     if(!n) {//closed successfully                                 
     return; 
     } 
     usleep(100); 
    } 
    printf("Close failed for %d\n", fd); 
    } 

KHÁCH HÀNG:

phía khách hàng đang sử dụng libcurl v 7.27.0

CURL *curl = curl_easy_init(); 
CURLcode res; 
curl_easy_setopt(curl, CURLOPT_URL, url); 
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); 
curl_easy_setopt(curl, CURLOPT_WRITEDATA, write_tag); 

res = curl_easy_perform(curl); 

Không có gì lạ mắt, chỉ cần một kết nối curl cơ bản. Khách hàng bị treo trong tranfer.c (trong libcurl) vì ổ cắm không được coi là bị đóng. Nó đang chờ đợi thêm dữ liệu từ máy chủ.

Những điều tôi đã cố gắng cho đến nay:

Shutdown trước khi đóng cửa

shutdown(fd, SHUT_WR);                                    
char buf[64];                                      
while(read(fd, buf, 64) > 0);                                   
/* then close */ 

Thiết SO_LINGER để đóng buộc trong 1 giây

struct linger l; 
l.l_onoff = 1; 
l.l_linger = 1; 
if (setsockopt(client_socket, SOL_SOCKET, SO_LINGER, &l, sizeof(l)) == -1) 
    abort(); 

Những đã thực hiện có sự khác biệt. bất kì ý kiến ​​nào đều được đánh giá cao.

EDIT - Điều này đã kết thúc là một vấn đề an toàn luồng bên trong thư viện hàng đợi khiến cho nhiều socket bị xử lý không thích hợp.

+0

Bạn có 100% tích cực không có chủ đề nào khác có thể sử dụng ổ cắm khi bạn gọi 'đóng' trên nó? Làm thế nào để bạn đọc không bị chặn? –

+0

Tôi e rằng tôi vừa đăng nhập ở đây và nhớ vấn đề này. Tôi phát hiện ra sau đó có một vấn đề an toàn thread trong một hàng đợi được sử dụng để vượt qua các kết nối xung quanh. Không có lỗi ở đây. Xin lỗi vì thông tin sai lạc. – DavidMFrey

Trả lời

54

Dưới đây là một số mã tôi đã sử dụng trên nhiều hệ thống Unix-like (ví dụ SunOS 4, SGI IRIX, HPUX 10.20, CentOS 5, Cygwin) để đóng socket:

int getSO_ERROR(int fd) { 
    int err = 1; 
    socklen_t len = sizeof err; 
    if (-1 == getsockopt(fd, SOL_SOCKET, SO_ERROR, (char *)&err, &len)) 
     FatalError("getSO_ERROR"); 
    if (err) 
     errno = err;    // set errno to the socket SO_ERROR 
    return err; 
} 

void closeSocket(int fd) {  // *not* the Windows closesocket() 
    if (fd >= 0) { 
     getSO_ERROR(fd); // first clear any errors, which can cause close to fail 
     if (shutdown(fd, SHUT_RDWR) < 0) // secondly, terminate the 'reliable' delivery 
     if (errno != ENOTCONN && errno != EINVAL) // SGI causes EINVAL 
      Perror("shutdown"); 
     if (close(fd) < 0) // finally call close() 
     Perror("close"); 
    } 
} 

Nhưng không ở trên không đảm bảo rằng bất kỳ ghi đệm được gửi.

Đóng cửa duyên dáng: Tôi mất khoảng 10 năm để tìm ra cách đóng một ổ cắm. Nhưng trong 10 năm nữa tôi chỉ lười biếng gọi là usleep(20000) cho một chút chậm trễ để 'đảm bảo' rằng bộ đệm ghi được xóa trước khi đóng. Điều này rõ ràng không phải là rất thông minh, bởi vì:

  • Thời gian trễ quá lâu.
  • Thời gian trễ quá ngắn - có thể!
  • Tín hiệu như SIGCHLD có thể xảy ra để kết thúc usleep() (nhưng tôi thường gọi là usleep() hai lần để xử lý trường hợp này - một hack).
  • Không có dấu hiệu cho biết điều này có hiệu quả hay không. Nhưng điều này có lẽ không quan trọng nếu a) hard reset hoàn toàn ok, và/hoặc b) bạn có quyền kiểm soát cả hai bên của liên kết.

Nhưng thực hiện việc tuôn ra thích hợp là đáng ngạc nhiên khó khăn. Sử dụng SO_LINGER rõ ràng là không phải là cách để đi; xem ví dụ:

SIOCOUTQ dường như là Linux cụ thể.

Lưu ý shutdown(fd, SHUT_WR)không viết dừng viết, trái với tên của nó và có thể trái với man 2 shutdown.

Mã này flushSocketBeforeClose() đợi cho đến khi đọc không có byte hoặc cho đến khi hết thời gian. Hàm haveInput() là một trình bao bọc đơn giản cho chọn (2) và được đặt để chặn tối đa 1/100 giây.

bool haveInput(int fd, double timeout) { 
    int status; 
    fd_set fds; 
    struct timeval tv; 
    FD_ZERO(&fds); 
    FD_SET(fd, &fds); 
    tv.tv_sec = (long)timeout; // cast needed for C++ 
    tv.tv_usec = (long)((timeout - tv.tv_sec) * 1000000); // 'suseconds_t' 

    while (1) { 
     if (!(status = select(fd + 1, &fds, 0, 0, &tv))) 
     return FALSE; 
     else if (status > 0 && FD_ISSET(fd, &fds)) 
     return TRUE; 
     else if (status > 0) 
     FatalError("I am confused"); 
     else if (errno != EINTR) 
     FatalError("select"); // tbd EBADF: man page "an error has occurred" 
    } 
} 

bool flushSocketBeforeClose(int fd, double timeout) { 
    const double start = getWallTimeEpoch(); 
    char discard[99]; 
    ASSERT(SHUT_WR == 1); 
    if (shutdown(fd, 1) != -1) 
     while (getWallTimeEpoch() < start + timeout) 
     while (haveInput(fd, 0.01)) // can block for 0.01 secs 
      if (!read(fd, discard, sizeof discard)) 
       return TRUE; // success! 
    return FALSE; 
} 

Ví dụ về sử dụng:

if (!flushSocketBeforeClose(fd, 2.0)) // can block for 2s 
     printf("Warning: Cannot gracefully close socket\n"); 
    closeSocket(fd); 

Ở phía trên, getWallTimeEpoch() của tôi cũng tương tự như time(),Perror() là một wrapper cho perror().

Edit: Một số nhận xét:

  • Lần nhập học đầu tiên của tôi hơi xấu hổ. OP và Nemo thách thức sự cần thiết phải xóa nội bộ so_error trước khi đóng, nhưng bây giờ tôi không thể tìm thấy bất kỳ tài liệu tham khảo cho việc này. Hệ thống được đề cập là HPUX 10.20. Sau khi thất bại connect(), chỉ cần gọi close() đã không phát hành bộ mô tả tệp, bởi vì hệ thống muốn cung cấp lỗi chưa xử lý cho tôi. Nhưng tôi, giống như hầu hết mọi người, không bao giờ bận tâm để kiểm tra giá trị trả lại của close. Vì vậy, tôi cuối cùng đã hết các tập tin mô tả (ulimit -n), mà cuối cùng đã nhận được sự chú ý của tôi.

  • (điểm rất nhỏ) Một người bình luận phản đối các đối số dạng số được mã hóa cứng đến shutdown(), thay vì ví dụ: SHUT_WR cho 1. Câu trả lời đơn giản nhất là Windows sử dụng # define/enums khác nhau, ví dụ: SD_SEND. Và nhiều nhà văn khác (ví dụ: Beej) sử dụng các hằng số, cũng như nhiều hệ thống cũ.

  • Ngoài ra, tôi luôn đặt FD_CLOEXEC trên tất cả các ổ cắm của mình, vì trong các ứng dụng của tôi, tôi không bao giờ muốn chúng được truyền cho một đứa trẻ, và quan trọng hơn là tôi không muốn một đứa trẻ bị ảnh hưởng.

Mẫu mã thiết CLOEXEC:

static void setFD_CLOEXEC(int fd) { 
     int status = fcntl(fd, F_GETFD, 0); 
     if (status >= 0) 
     status = fcntl(fd, F_SETFD, status | FD_CLOEXEC); 
     if (status < 0) 
     Perror("Error getting/setting socket FD_CLOEXEC flags"); 
    } 
+5

Tôi ước tôi có thể bỏ phiếu này hai lần. Đây chỉ là mẫu thứ hai của một ổ cắm được đóng đúng cách mà tôi đã thấy trong tự nhiên. – grieve

+1

+1 cho 'getockopt()' ing 'SO_ERROR'. – alk

+0

@JosephQuinsey - Bạn có tham chiếu về "lỗi ... sẽ khiến đóng() bị bỏ qua"? Tốt hơn là từ thông số POSIX? – Nemo

0

này nghe có vẻ với tôi như một lỗi trong bản phân phối Linux của bạn.

Các GNU C library documentation nói:

Khi bạn đã hoàn thành việc sử dụng một ổ cắm, bạn chỉ có thể đóng bộ mô tả tập tin với close

Không có gì về thanh toán bù trừ bất kỳ cờ lỗi hoặc chờ đợi cho các dữ liệu đỏ mặt hoặc bất kỳ thứ gì như vậy.

Mã của bạn là tốt; O/S của bạn có lỗi.

+0

Dựa vào câu trả lời này. Nó sẽ mất một số công việc để có được một os khác để kiểm tra. Tôi sẽ xem lại điều này khi tôi đã kiểm tra. Tôi muốn thêm liên kết này từ @Nemo vì có vẻ như có liên quan đến câu hỏi. và phản hồi được đính kèm đã bị xóa. https://sites.google.com/site/michaelsafyan/software-engineering/checkforeintrwheninvokingclosethinkagain – DavidMFrey

+0

Tôi chấp nhận câu trả lời này, vì việc thay đổi hàng đợi an toàn theo chủ đề của tôi để sử dụng các ẩn dụ thay vì điều kiện pthread không thể giải thích được (cho chính tôi) đã giải quyết vấn đề. – DavidMFrey

+3

'Không có gì về việc xóa bất kỳ cờ báo lỗi nào hoặc đang chờ dữ liệu bị xóa hoặc bất kỳ điều gì như vậy. 'Có thể cho rằng," đang chờ dữ liệu bị xóa "rơi xuống" khi bạn đã sử dụng xong ổ cắm ". –

2

Câu trả lời hay từ Joseph Quinsey. Tôi có nhận xét về chức năng haveInput. Tự hỏi khả năng chọn lựa đó trả về một fd bạn không bao gồm trong bộ của bạn như thế nào. Đây sẽ là một lỗi hệ điều hành chính của IMHO. Đó là loại điều tôi sẽ kiểm tra nếu tôi đã viết bài kiểm tra đơn vị cho các chức năng select, không phải trong một ứng dụng thông thường.

if (!(status = select(fd + 1, &fds, 0, 0, &tv))) 
    return FALSE; 
else if (status > 0 && FD_ISSET(fd, &fds)) 
    return TRUE; 
else if (status > 0) 
    FatalError("I am confused"); // <--- fd unknown to function 

Nhận xét khác của tôi liên quan đến việc xử lý EINTR. Về lý thuyết, bạn có thể bị kẹt trong một vòng lặp vô hạn nếu select tiếp tục trả về EINTR, vì lỗi này cho phép vòng lặp bắt đầu lại. Do thời gian chờ rất ngắn (0,01), nó dường như rất khó xảy ra. Tuy nhiên, tôi nghĩ cách thích hợp để giải quyết vấn đề này là trả lại lỗi cho người gọi (flushSocketBeforeClose).Người gọi có thể tiếp tục gọi số haveInput miễn là hết thời gian chờ của nó và tuyên bố lỗi cho các lỗi khác.

BỔ SUNG # 1

flushSocketBeforeClose sẽ không thoát khỏi một cách nhanh chóng trong trường hợp read trả lại một lỗi. Nó sẽ tiếp tục lặp cho đến khi hết thời gian chờ. Bạn không thể dựa vào số select bên trong haveInput để dự đoán tất cả các lỗi. read có lỗi của riêng mình (ví dụ: EIO).

 while (haveInput(fd, 0.01)) 
     if (!read(fd, discard, sizeof discard)) <-- -1 does not end loop 
      return TRUE;